One web component to rule them all?
Web components are pretty neat. They allow us to create and manage custom HTML elements, extend existing semantic elements, and even offer ways to create encapsulated portions of a component’s HTML (in what’s called a “shadow” DOM) that can’t be impacted by global styles and scripts.
While web components are capable of managing complex functionality, I appreciate that they don’t require that we use their more advanced features when we don’t need them. For example, we can construct web components in the regular “light” DOM by starting with regular server-delivered HTML, which is great for performance and resilience and allows easy styling in CSS. Building web components this way may seem like a typical progressive enhancement pattern, but one handy feature sets them apart: lifecycle callbacks, which allow us to run scripting at moments like when an element appears-in and disappears-from the DOM, or when an HTML attribute changes.
To demonstrate a couple of lifestyle callbacks, here’s a quick example of a component called <a-component/>
with a little scripting that uses the connected
and disconnected
callbacks to run whenever the element is added or removed from the DOM.
First, the HTML:
<a-component>
<p>Content goes here</p>
</a-component>
Then some JS:
export class Component extends HTMLElement {
connectedCallback(){
alert(this + "has entered the DOM!");
}
disconnectedCallback(){
alert(this + "has left the DOM!");
}
}
if ('customElements' in window) {
customElements.define('a-component', Component );
}
As you can hopefully discern in that nifty new “class” syntax, we’ve defined a custom element called a-component
and thrown some window alerts when that element is found in the DOM and when it leaves the DOM. That’s a convenient little upgrade compared to the old patterns of say, waiting until the DOM is ready and then finding elements and looping over them to apply behavior. It also seems that this definition will work when applied either up-front or after HTML is created, which means there aren’t race conditions to worry about with regards to loading the scripts. Subsequently, that also means these components play nicely with client-side rendering patterns as well, where elements routinely appear and disappear from the DOM long after the page is initially delivered. (Which is to say, it works with elements rendered into the DOM via frameworks like Vue or React).
Maybe most importantly to us, this pattern provides a nice hook for adding progressive enhancements to already-meaningful HTML contained in these custom elements, leaving them resilient in the case of of script loading failures and allowing the page to start rendering before the JS happens to run.
Building a factory
Permalink to 'Building a factory'As nice as that lifecycle is, one drawback I find in the web component syntax is that it tends to feel structured around one particular component’s behavior. On a typical website, we sometimes add many small behavioral script enhancements to a single element that may or may not relate to each other, and in this way web components can feel a little stifling. For example, a button that primarily toggles its sibling’s visibility on click might also have some unrelated behavior on mouse-hover or focus, like a tooltip, but to do that with web components you’d need to make a single component that does both. To see what was possible, I set out to use this pattern to delegate one or many individual behaviors instead of just one. Here’s what I came up with…
For the following example, I’ve placed a slightly improved version of the above web component script into a file called component.js
. You can view it here if you’d like. That script is still set up to register elements named a-component
, but it also looks for a does
attribute on the elements themselves, and that attribute accepts one or more JavaScript named classes (one
and two
in this particular example). Those named classes, if imported and defined in the page, are expected to have constructor
and destructor
methods, which will be called on the element via the connected and disconnected life cycle callbacks. Essentially, the behaviors we add to the element can now reside in these named class files instead of the main component, so one
and two
might really add click toggle and hover tooltip behaviors in a real site. Here’s how that looks from the code:
First, the HTML:
<script type="module">
import {one} from './one.js';
window.one = one;
import {two} from './two.js';
window.two = two;
</script>
<script type="module">
import './component.js';
</script>
<a-component does="one two">
<p>Content goes here</p>
</a-component>
And the JS for one.js
:
export class one {
constructor(elem){
this.elem = elem;
alert("create one")
}
destructor(){
alert("destroy one")
}
}
And two.js
:
export class two {
constructor(elem){
this.elem = elem;
alert("create two")
}
destructor(){
alert("destroy two")
}
}
For sake of brevity, you can view the completed component.js script externally here. It contains a little more code than the initial example, but for its size, I think it makes for a nice little behavior delegator, complete with a “defined” HTML class that can be used to apply CSS when the element is enhanced (a stand-in for the standard ":defined
pseudoclass which doesn’t work in polyfilled browsers), and some events that trigger upon “create” and “destroy”, respectively.
With this little component factory in play, we can apply small scripts that do particular things throughout the HTML, and know that they’ll enhance at the right moments regardless of whether the site is fairly traditional or designed as a long-lived single page app.
Practically Speaking
Permalink to 'Practically Speaking'I’m not sure yet if this little pattern is something we’ll end up using in a project, so to help answer that question I’ve started to kick the tires on it by converting some components to it from existing ones that we’ve used for a while. Here’s a proof of concept demo of our “Snapper” snap-points carousel running on it, for example: demo page | JS
Support
Permalink to 'Support'I should also note that while web components browser support is great, their various APIs do need polyfills to work in IE11. In my demo pages so far, I’ve been including the custom element polyfill by the talented Andrea Giammarchi like this:
<script>this.customElements||document.write('<script src="//unpkg.com/document-register-element"><\x2fscript>');</script>
Using the is
attribute
Permalink to 'Using the is attribute'
Another caveat: one of the things I was hoping I could do with this pattern is to use the a-component
with the is
attribute, which is the way that web components allow us to extend semantic elements that already have native behavior. Using a-component
via is
could look like this:
<select is="a-component" does="one two">...</select>
I was able to get this to work, but unfortunately, to make a web component class register for is
usage like this you need to extend specific elements by name, which would mean my component.js would need to extend HTMLSelectElement
specifically (and all other elements that I might want to add behaviors to) instead of the generic HTMLElement
. I wasn’t able to find a workaround yet that let me do this without something silly like looping and extending every possible element, but I’m open to ideas. For now, it’ll have to apply via a wrapper element, which isn’t a terrible compromise. And while I’m here, I should note that Andrea also has a great IS
polyfill for browsers that don’t support that pattern, including Safari.
Thoughts?
Permalink to 'Thoughts?'Thanks for following along! This is our first foray into doing much of anything with web components so we’d love to hear your thoughts on whether this might be useful pattern, though it’d be useful to know if you think it’s a terrible idea too! For any feedback, you can reach out to us on twitter