A Wicked Custom Elements Alternative

Andrea Giammarchi
6 min readNov 19, 2018
Photo by Jonatan Pie on Unsplash

Update: don’t miss V1 of this library, which is even smaller, and better tested!

TL;DR I’ve smashed disconnected, attributechanged, and regularElements modules together to create an intriguing lightweight wrapper (roughly 2Kb all included) that lets you define Custom Elements like Components via CSS queries, working down to IE9, and without needing any polyfill at all.

It’s called wickedElements, and it exposes the same customElements API through the handleEvent pattern, powered by prototypal inheritance.

And even if I wish I made this utility years ago, let’s see what the hell actually brought me to write it, shall we?

React hooks are cool, but not “wicked-cool”!

While playing around with Neverland, aiming at its latest version, I’ve noticed that the useEffect hook easily leads to unnecessary computational overhead.

It is true that useEffect simplifies some React specific paradigm, but at the same time it’s triggered way too much, becoming easily inefficient.

For instance, if you remove a listener, a subscription, or a login action, and add whatever it was back every single time you update a value of the state, the ease of use might backfire with network requests overhead.

With hyperHTML though, the mechanism to understand when a bound or a wired content goes live or not, is to use either onconnected=${...} or ondisconnected=${...} special attributes events.

This will grant you that, whatever thing you are subscribing to, it won’t happen more than once per element life-cycle, letting the smart v-dom-less differ behind the scene ensure that no element ever leaves the DOM unless, of course, it’s meant to.

However, until few days ago, that magic was part of hyperHTML core logic, and there were also no modules focused on solving only that life-cycle issue, which is why I’ve came up with my own solution but lately realized that … 🥁🥁🥁 … it was buggy! 🎉

From Bug To Profit

One thing you should know about me, is that I don’t only use Twitter to rant (ok, ok, I kinda do that too often, I know, I’m working on it … but …)

I also usually implement everything I need, and most of the time propose it to the community, or standard bodies, as Open Source effort.

Differently from every other time though, this one was special.

We use hyperHTML in the ABP Extension, and through HyperHTMLElement custom elements, reaching millions of active users daily.

Even if that means that hyperHTML is really battle tested, we all know no software is perfect, especially when it comes to deal with tons of DOM quirks, often even different per each browser.

And indeed, not too surprisingly, I have found recently a bug in hyperHTML and wasted quite some time to solve it.

However, such issue wasn’t really strictly hyperHTML related, and since I’ve been advocating standards for 18+ years, and I do respect developers using just the platform and few minimal helpers, I’ve told myself: “how about you refactor this bit out and let everyone use it as solution to that problem?

And so the module disconnected was born: a super tiny function that makes any handled node capable of listening to connected or disconnected events.

Now, this is cool and everything, but not Custom Elements cool, ’cause I still was missing the ability to listen to attributes changes.

With hyperHTML, that is implicit per each render, as long as the attribute value is different from the previous one.

Since hyperHTML is declarative, having hooks about attributes changes is also not super interesting ’cause you declare attributes and you know their values.

However, with HyperHTMLElement based Custom Elements, we use attributes to setup our components quite often, and indeed, the benefit of using Custom Elements is mostly summarized by the ability to define a connectedCallback, a disconnectedCallback, and an attributeChangedCallback.

These are methods, not listeners, and as such, these are also exposed through any upgraded custom elements (aka, to some degree, these are leaky).

I also kinda think the chosen name is pretty awkward, but I understand the need to make it clear what are those methods about (yet, the Callback suffix, for methods defined in an already class-able EcmaScript era, where nobody would name each method ...Callback look, I’m afraid, kinda awkward).

Back to the topic, attributechanged was the extra missing piece to simulate, through an even better, or more powerful way, Custom Elements.

Why better, huh?”, you might ask, and the reason is that you are free to listen to any attribute, as opposite so listen to only few of them, and yet you have the ability, if necessary, to specify which attribute you like.

Adding regularElements to the mix

Well, lifecycle are in, attributes are in, what else did I miss? Oh yeah, a way to register some element and automatically bring these capabilities in it.

However, while I don’t like much how Custom Elements compose, I do like that DOM nodes have always played well with CSS, so that a single node could be styled by more selectors, composing its view through multiple players.

And what could be better at defining logical behavior, as opposite of visual one, if not the very same CSS selectors to reach these nodes?

So here I am, with less than 150 LOC, and a new custom elements like registry based on CSS selectors and no need for classes.

The logic behind? The same used by disconnected, but only for the initial, one-off, setup, after which disconnected and attributechanged take care of everything else.

A registry to define only Custom Elements hooks is minimal but also limited, and those callbacks invoked with the same node as context, when any event already carries the currentTarget property which always refers to the element in which the listener was added, looked like slightly wasted slot.

What if I want to define more listeners and not just those hooks?

What if I want to setup any element/component once only, the most common use case I know to date about any custom element or component?

What if I want to relate a component to an element, without ever leaking such component to the outer world, having the freedom to attach anything to it, with the ability to reach its related element any time?

What if the following piece of code is everything I ever needed?

define('.this-is-wicked', {
// currently the default init already
init(event) {
this.el = event.currentTarget;
},
// any `on` prefixed method set once as listener
onclick() {
alert('clicked ' + this.el);
},
// Custom Elements hooks included
onattributechanged(event) {
const {attributeName, oldValue, newValue} = event;
// ...
},
onconnected(event) {
console.log(this.el.nodeName);
}
});

The wickedElements Finale

Not named by accident, it’s entirely based on regularElements, bringing those Custom Elements ability, through a register of selectors, and using prototypal inheritance and the handleEvent pattern to have CPU and RAM friendly components that needs zero classes and that works per behavior, where any behavior can be defined by any CSS selector.

If you don’t believe me, please see the demo source code, or test it anywhere you like, just adding https://unpkg.com/wicked-elements script to the mix, and I am sure you’ll be blown away by how wicked these components are!

Strawberry on top? hyperHTML would work out of the box there too, and so would any other library, included React, if you have a way to reach via CSS its components.

This little, quick, and innovative way to deal with components reminds me that we have already pretty cool primitives on the native DOM world, so that using these the right way might create new enjoyable to explore adventures.

I hope you’ll try some wicked component out, enjoying its utter simplicity.

P.S. No, you don’t need hyperHTML to use this library. You are free from everything else, just go wild with the bare DOM as you, or your server, knows.

P.P.S. you can try this live showcase playground too

a code pen screenshot

--

--

Andrea Giammarchi

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.