Different ways of defining custom elements
I’ve developed a habit of defaulting to writing “wrapper” elements any time I need some dynamic behavior on any element. If I need a click handler on a button, I will do this 9/10 times:
<my-butt>
<button>hello</button>
</my-butt>
What’s interesting is the JS part that comes after this. For me, it’s almost always an inline anonymous class:
customElements.define("my-butt", class extends HTMLElement {});
That line is quite long, but it skips a step and avoids the need to name the class. Here’s the “regular” version:
class MyButt extends HTMLElement {}
customElements.define("my-butt", MyButt);
Another equivalent approach would be to use a static block, which I think reads nicer.
class MyButt extends HTMLElement {
static {
customElements.define("my-butt", this);
}
}
This works perfectly for one-off custom elements, but it’s not very flexible and the consumer has no control over it. For reusable custom elements, it’s better to expose a static method that can be manually called (e.g. MyButt.register()
).
class MyButt extends HTMLElement {
static register() {
customElements.define("my-butt", this);
}
}
However, there is still no way to customize the “tag name”. What if my-butt
is already occupied? A reusable element needs to allow registering itself with a different tag name.
We can allow passing the tag’s name as an optional parameter in our static register
method.
class MyButt extends HTMLElement {
static register(tagName = "my-butt") {
customElements.define(tagName, this);
}
}
Then call it like this:
MyButt.register("my-button");
A slightly different approach would be to use a static property. We can name it tagName
to match the read-only tagName
property of HTMLElement
.
class MyButt extends HTMLElement {
static tagName = "my-butt";
static register() {
customElements.define(this.tagName, this);
}
}
Then it can be changed before registering the custom element.
MyButt.tagName = "my-button";
MyButt.register();
<my-button>
<button>hello</button>
</my-button>
Differences between static and instance properties
One interesting difference is that the instance tagName
property is always returned as uppercase.
static init() {
console.log(this.tagName); // my-button
}
connectedCallback() {
console.log(this.tagName); // MY-BUTTON
}
It would be nice if we could match the static property to also be uppercase. Sadly, we cannot define a custom element with an uppercase name.
// ❌ not allowed
customElements.define("MY-BUTTON", MyButt);
We can however convert the string to lowercase before defining the custom element!
class MyButt extends HTMLElement {
static tagName = "MY-BUTT";
static register() {
console.log(this.tagName); // MY-BUTT
customElements.define(this.tagName.toLowerCase(), this);
}
connectedCallback() {
console.log(this.tagName); // MY-BUTT
}
}
Is this worth it? I’m not sure.
The benefit of the static property starts to become more evident when we use a shared base class across multiple elements. The register
method stays the same between sub-classes, so only the property needs to be set.
Combining it all:
class Base extends HTMLElement {
static register(tagName = this.tagName) {
customElements.define(tagName, this);
}
}
class MyButt extends Base {
static tagName = "my-butt";
}
class MyToggle extends Base {
static tagName = "my-toggle";
}
class MyThing extends Base {
static tagName = "my-thing";
}
If all our custom elements are coded this way, we can even register them all at the same time in a loop.
[MyButt, MyToggle, MyThing].forEach((element) => element.register());