My first custom element
Published
After some years of browser vendors working out what web components should look like, they're almost ready for the prime time. The part which I find most intriguing (custom elements) has finally stabilised. With custom elements, you can make new HTML elements which have custom behaviour which you define using JavaScript. In this post I'll demonstrate a custom element for fuzzy counting.
Custom elements are created in two parts. Firstly we need to extend an element with a JavaScript class.
class FuzzyCount extends HTMLElement {
}
So far this describes no custom behaviour. All this is is the extended class,
with identical behaviour to an HTMLElement
. We can customize the constructor
to add the behaviour we want. In order to give the element data to use, it must
be passed in as an attribute. We'll use an attribute called count
.
class FuzzyCount extends HTMLElement {
constructor() {
// The parent constructor must be called
// before using `this`.
super();
// rawCount is a string.
const rawCount = this.getAttribute('count');
const count = parseInt(rawCount, 10);
// Set text content based on the count.
if (count === 0) {
this.textContent = 'none';
} else if (count === 1) {
this.textContent = 'one';
} else if (count === 2) {
this.textContent = 'a couple';
} else if (count < 5) {
this.textContent = 'a few';
} else if (count < 10) {
this.textContent = 'several';
} else {
this.textContent = 'lots';
}
}
}
The element reads and parses the count
attribute, giving itself a text content
accordingly.
To make the element available to a page, it must be registered. One interesting
restriction placed upon custom elements is that they must contain a hyphen so
that they can be distinguished from built-in elements. We're going to register
our element as fuzzy-count
, but in the real world you should prefix it with
a namespace. For example, if we're working at a place called Funky Corp, we
could name the element funkycorp-fuzzy-count
.
customElements.define('fuzzy-count', FuzzyCount);
Now we can use this to make elements on the page!
<fuzzy-count count="3"></fuzzy-count>
Any matching custom elements which exist before the element is defined will be upgraded. This means a page can be sent by the server with custom elements included, and everything will be rendered properly once the custom element is registered.
But what if we want to create elements in JS? We can try:
const fuzzyCount = document.createElement('fuzzy-count');
but an error will be thrown. It turns out that newly constructed elements cannot
contain other stuff (the text content). Even if no error was thrown, we'd
have an element with no count
attribute, and so we've missed our chance since
all the logic is in the constructor. It turns out that the constructor is the
wrong place for this stuff.
Thankfully we can make a few changes to defer the setting of text content.
class FuzzyCount extends HTMLElement {
// Called when the element is inserted into
// the document or upgraded.
connectedCallback() {
this.setTextContent();
}
setTextContent() {
const rawCount = this.getAttribute('count');
if (!rawCount) {
this.textContent = '';
return;
}
const count = parseInt(rawCount, 10);
if (count === 0) {
this.textContent = 'none';
} else if (count === 1) {
this.textContent = 'one';
} else if (count === 2) {
this.textContent = 'a couple';
} else if (count < 5) {
this.textContent = 'a few';
} else if (count < 10) {
this.textContent = 'several';
} else {
this.textContent = 'lots';
}
}
}
The logic which sets the textContent
has moved to its own method
setTextContent
, and connectedCallback
calls it when the element is
"connected" to avoid the error when constructing the element in JS. Connected is
called when the element is inserted into the document, or the element is
upgraded by custom element registration.
This is enough to get the behaviour we want, so long as the count attribute
of a new fuzzy-count
element is set before it is appended to the document.
const fuzzy = document.createElement('fuzzy-count');
fuzzy.setAttribute('count', 10);
document.body.appendChild(fuzzy);
We can take one more step to make the element react to changes to the count attribute...
class FuzzyCount extends HTMLElement {
// Called for each watched attribute when
// the element is added to the document or
// upgraded. Also fired for a watched
// attribute when it is added, updated, or
// removed.
attributeChangedCallback() {
this.setTextContent();
}
// A static getter because we can't add a
// static value within a class declaration
// directly (yet).
static get observedAttributes() {
return ['count'];
}
setTextContent() {
// Same as before.
}
}
connectedCallback
has been replaced with attributeChangedCallback
, and a
static getter defines the list of attributes to watch. For each watched
attribute, when an element is inserted, upgraded, or the attribute is added,
updated, or removed, this attributeChangedCallback
is called. These changes
allow us to update elements before appending as before, and also after
appending.
const fuzzy = document.createElement('fuzzy-count');
document.body.appendChild(fuzzy);
fuzzy.setAttribute('count', 10);
Overall I'm impressed. I've only covered some of the API available here. It is a little fiddly, but upon consideration it all seems to make sense so far. It strikes me that a good way to use it would be for small components, or as a low level primitive for a front end framework.
Finally, a note on compatibility. At the time of writing Chrome and Safari support custom elements with no additional effort. Other browsers must be polyfilled for support.