Any Holy Grail for Web Components?

The history of Web Components doesn’t really shine so far:

  • their declarative nature is confined in the static HTML world
  • the JS counter-declaration, through slots and Shadow DOM is awkward
  • Shadow DOM is both heavy and hard to reliably polyfill
  • Shadow DOM doesn’t even solve all problems, plus it needs mandatory setup, and it cannot be delivered through SSR (I know it shouldn’t … but!)
  • HTML Modules are still nowhere, but also not too different from what Vue.js or other frameworks offer already since years

If we don’t need or use Shadow DOM, we have better chances our components will be widely adopted, as it’s been for most successful libraries and projects to date, and yet we’d be left with other pain points:

  • the declaration is still statically confined in the HTML world
  • CSS are elsewhere too, and more fragile than those within Shadow DOM
  • the amount of redundant nodes will be too damn high, so more complex pages than needed will be heavier to both download and handle
  • the integration with good old standard DOM and themes will still be awkward

About DOM Redundancy

If we’d like to represent a paragraph that might also inherit themes from the hosting page, and we want to deliver it as “special paragraph”, capable of doing magic things via JS, we’d probably go like this:

// the JS side
class SpecialP extends HTMLElement {
constructor() { /* setup it once */ }
}
customElements.define('special-p', SpecialP);
// the HTML side
<special-p>
<p>
The actual content
</p>
</special-p>

Using Shadow DOM instead, or in any form, would simply bloat both logic and layout, without even providing a <p> tag that can reflect the hosting site theme.

Talking also about graceful enhancement, browsers without Custom Elements support won’t even know what to do with that <special-p> tag.

All we wanted was just an easy way to setup one, or thousand, paragraphs.

A standard based approach

The tiny 2K Wicked Elements library is already an awesome solution for the described issue, because it requires zero polyfills, it work down to IE9, and it exposes Custom Elements like events for any element that reaches the DOM, without using Custom Elements at all, through few other available APIs.

// the JS side
wickedElements.define('p.special', {
init() { /* setup it once */ }
});
// the HTML side
<p class="special">
The actual content
</p>

This should be a no-brainer for every developer, and I strongly suggest you to give this tiny library a chance, but we’re still left with few gotchas:

  • the wickedElements library works well for those with a good understanding of the stack, but it might be slightly confusing for newcomers (being based on a completely different than usual paradigm)
  • elements are upgraded on the fly, once live, but the DOM knows nothing about these elements until the upgrade happens. This is also true for Custom Elements, but using native primitives, such as Custom Elements, should be always a safer bet when it comes to internal, or future, browser performance optimizations
  • the HTML content declaration is, by default, still confined in the HTML world. This is awesome when it comes to SSR and graceful enhancement, but the React ecosystem showed that many developers like to confine their logic only in one place: the JS file, through JSX (or similar looking solutions)

Quick Recap: The React Dev Experience

Before reading further about Web standards, I’d like to summarize what I believe helped making React so popular:

  • great documentation
  • great tooling around
  • you import dependencies and declare your component intent in one file

Web standards surely covers as well, if not better, the first two points in this list, but these are incapable of beating the 3rd one:

// the React JS side
class Special extends React.Component {
render() {
return <p>{this.props.text}</p>;
}
}
// still the React JS side ...
const el = <Special text="The actual content" />;

Add styled components to the mix, and that’s it … is there any chance we could go even close to such experience and ease via standards?

The Custom Elements Builtins Approach

Unfortunately not yet available in every browser, but with a polyfill that weights just 1K for those with already customElements support (i.e. Safari), this primitive is the best compromise to obtain the power of wickedElements through the standard expressiveness of Custom Elements.

// the JS side
class SpecialP extends HTMLParagraphElement {
constructor() { /* setup it once */ }
}
customElements.define('special-p', SpecialP, {extends: 'p'});
// the HTML side
<p is="special-p">
The actual content
</p>

As a result, we’ve already solved few pain points previously described:

  • there is no DOM bloat whatsoever
  • the page is graceful enhanced
  • the page is SSR friendly out of the box
  • we have all life cycles we like from Custom Elements

Accordingly, the only things that are missing are:

  • a way to declare content also within the JS file
  • a way to define components styles also within the JS file
  • some usual extra magic that helps developing great apps with ease

In few words, the so-far pushed back builtin extends, combined with some user-land standard based solution, to create declarative layouts in JS, might be the perfect combo to simulate the React experience in the standard world.

What a Heresy !

I think this library name couldn’t be more appropriate: it brings together concepts usually profoundly at odds with what is generally accepted.

Following the most basic example for the special P component:

// the JS side
import {render, html, define} from 'heresy';
define(class Special extends HTMLParagraphElement {
static tagName = 'p';
});
// the HTML side (SSR ready)
<p is="special-heresy">
The actual content
</p>
// or ... the JS side
render(document.body, () => html`
<Special>
The actual content
</Special>
`);

🤯

Wait … what ?

  • the static class .name is used to define the registry name, suffixed by -heresy , making any name always valid for the custom elements registry
  • the static class tagName is used to specify what kind of real DOM element it should represent. Being a custom element builtin, it could be literally any element (option, tr, td, select, li, input, label, …)
  • if found in the DOM through is= attribute, it’ll be upgraded right away
  • if written through the class .name via lighterhtml, the engine used behind the scene to declare the layout via template literals, it’ll be converted automatically as <p is="..."> so that no bloat exists, declared nodes are actually live on the DOM (you can even query these) and there’s nothing virtual: the custom element is the instance you’re handling.

As result, we can declare in a way that looks very similar to JSX.

but … what about CSS ?

import {render, html, define} from 'heresy';
define(class Special extends HTMLParagraphElement {
static style = selector => `
${selector} {
transition: background 300ms;
}
${selector}:hover {
background-color: silver;
}
`;
static tagName = 'p';
});

The CSS is injected through the class static style method and only once per class declaration, passing along a selector that is simply a string containing the name used to style the component.

In this example, that’d be the string p[is="special-heresy"] .

Being just a static method, style could return text transpiled at runtime via SASS, or LESS, among other transformers.

but … what about props?

import {render, html, define} from 'heresy';
define(class Special extends HTMLParagraphElement {
static tagName = 'p';
#props = {};
get props() { return this.#props; }
set props(props) {
this.#props = props;
this.render();
}
this.render() {
this.html`${this.props.text}`;
}
});
render(document.body, () => html`
<Special props=${{text: 'The actual content'}}/>
`);

Thanks to lighterhtml, Custom Elements can have any getter or setter specified, so that any kind of value can be passed through right away.

but … what about compatibility?

Using the right combination of polyfills and tools, heresy works down to IE9.

Check the basic live demo, or its source code, to udnerstand how to do that.

but … what about … ?

Please give me a break 😂

The project is still in its early days and kinda experimental, but there is already some promising looking outcome, like this classic todo demo (source), which would currently run only in Chrome Canary because it’s based on all latest JS features without using any transpilation.

Things like this:

// ...
render() {
this.html`
<input placeholder="type item" onkeydown=${this}>
<Hide onchange=${this}/>
<ul>${this.items.map(
data => html`<Item data=${data}/>`
)}</ul>`;
}

are what make this Custom Elements based approach absolutely a pleasure to work with, but bear in mind so far I haven’t yet created any concrete app with it, and I’m still wondering if hyperHTML, instead of lighterhtml, would be more appropriate, thanks to its .bind mechanism.

Since lighterhtml is the easiest tool ever for prototyping though, I think for the time being all I want to understand is if the community likes, and is interested, into adopting such solution so that maybe, one day, Safari devs will provide native builtins too and we can all drop the polyfill for it.

So please let me know on twitter what you think about this heresy, and thanks a lot for reading until the end ❤️