This is some sort of answer to recent posts regarding Web Components, where more than a few misconceptions were delivered as fact.
Let's start by defining what we are talking about.
As you can read in the dedicated GitHub page, Web Components is a group of features, where each feature works already by itself, and it doesn't need other features of the group to be already usable, or useful.
You don't need to use all the Web Components features to create, and ship, robust Custom Elements for your project, or in the wild, you need to understand all parts are unrelated.
- Shadow DOM works for every node already, you don't need Custom Elements to use it, it's a spec a part
- Custom Elements work everywhere already, and you don't need Shadow DOM to define custom elements
- HTML Templates are super useful nodes, and not only already parked in the layout. Every library dealing with runtime DOM creation uses these to reliably create any sort of DOM node. And you don't need even this to have Custom Elements.
- CSS changes are not fully there yet but it doesn't matter. If your project works without custom elements but with reliable styling, you can apply the exact same toolchain/technique to Custom Elements: they are literally just elements!
- HTML Modules are also not too interesting yet, as I believe module mapping would gain more traction. And yet, you don't need HTML Modules to ship Custom Elements.
Now, following the most recent post I've read on this argument, but it's similar to other posts recently published here and there, I'd like to address all points.
The TL;DR answer is that yes, you can extend builtin elements, and I have no idea for how many years I've shouted that.
In other words, a bog-standard
<a>
element, in all its accessible glory.
Here you go:
customElements.define(
'twitter-share',
class TwitterShare extends HTMLAnchorElement {
static get observedAttributes() {
return ['text', 'url', 'hashtags', 'via', 'related'];
}
connectedCallback() { this.addEventListener('click', this); }
attributeChangedCallback() {
this.setAttribute('noreferrer', '');
this.textContent = 'Tweet this';
this.href = 'https://twitter.com/intent/tweet?' +
getQueryString(this, TwitterShare.observedAttributes);
}
handleEvent(e) { this['on' + e.type](e); }
onclick(e) {
e.preventDefault();
const w = 600;
const h = 400;
const x = (screen.width - w) / 2;
const y = (screen.height - h) / 2;
const features = `width=${w},height=${h},left=${x},top=${y}`;
window.open(this.href, '_blank', features);
}
},
{extends: 'a'}
);
Now the component in the page can just be a link:
<a is="twitter-share"
text="A Twitter share button with progressive enhancement"
url="https://codepen.io/WebReflection/pen/LKWyLB?editors=0010"
via="webreflection"
/>
It could also have a fallback href
in case things go really wrong, or such href
could be generated directly on the server, so that the client will receive just:
<a is="twitter-share" href="....">Tweet this</a>
That'd be automatically hydrated on the client, once the component is registered, and the functionality is preserved, 'cause no observable attributes are used.
You can try above example in Code Pen.
There's the possibility to have the magic svelte-class too, because indeed that's just a link, and svelte can use the link.
Accordingly, since custom elements builtins are possible, is there any other argument against Custom Elements and progressive enhancement, since these also provide automatic hydration when rendered through the server?
Is there anything more semantic and lightweight out there? (beside wickedElements, of course)
With little helpers such as heresy-ssr, you can serve clean components with rehydration on the client that costs 0.
Check the repository out, then npm run build
then node test/twitter-share.js
and see the clean SSR component delivered, and rehydrated later on through the client side definition.
If you want to use Shadow DOM for style encapsulation ...
While I hope it's clear by now nobody needs Shadow DOM to style components, there are many other ways to better style any layout without needing Shadow DOM.
Not only Svelte is compatible out of the box with nested styles, where I work we also have various io-prefixed Custom Elements (no shadow dom, no modules, just definitions via HyperHTMLELement), and all of them with their own css files, with bundling taking care of the rest: CSS in style, JS in script.
However, even a generic Custom Element could do the same: contain its own CSS definition, and still use the whole document to inject the proper style, when and if needed, instead of letting the server do that.
With this technique the CSS might be included in the Custom Element, but it will also enable CSS on demand, once per custom element definition, as example, so that you don't need the bloat of every component upfront: you can just download these on demand and see the style applied at distance.
I know this also the general purpose of HTML Modules, but all I wanted to say is that we already have the ability to bundle Custom Elements together with their style, if needed, and their HTML or SVG content, something easily provided by my libs, such as lighterhtml or its booster heresy, but also by many others.
I am often disappointed by the way new features are presented, specially if advertised prematurely without other vendors consensus, as it was for Custom Elements V0, which surely didn't help the Web Components Umbrella shine.
However, what we have now, is the ability to use Custom Elements V1 everywhere, and this is huge!!!
Not only my polyfill from 2014 worked in every IE8+ and mobile browser out there, without native customElements
, but today all we optionally need is builtin extends in Safari, and none of these two poly will ever land in evergreen Chrome, Firefox, or Edge on Chrome.
<script>
if (this.customElements) {
try {
// Safari fails here 'cause they poisoned every native constructor
customElements.define('built-in', document.createElement('p').constructor, {'extends':'p'});
} catch(_) {
// This will land only in Safari, until they fix their gap with others
// The poly is ~1K and based on native Custom Elements V1 API
document.write('<script src="//unpkg.com/@ungap/custom-elements-builtin"><\x2fscript>');
}
} else {
// legacy browsers only, including: IE8, IE9, IE10, IE11, MSEdge and very old mobile phones
document.write('<script src="//unpkg.com/document-register-element"><\x2fscript>');
}
</script>
<script>
// everything else that needs a reliable customElements global
// with built-in extends capabilities
</script>
That is it: it's zero bloat for 70% of the users, ~1K extra bloat for Safari devices until builtin extends are in, and still few Ks for legacy only, perfectly capable of working with custom elements too 🎉
So, considering the features already shipped, and polyfills cover everything since 2014 or before, why don't we start using Custom Elements instead of keep demanding new features?
This is probably one of the most controversial parts of the whole Web Components history:
- the Shadow DOM polyfill is IMO not worth it, if you want a spec compliant Shadow DOM. It's not by accident that all my polyfills are about what's possible to polyfill, which is Custom Elements V0 and V1. Indeed you likely don't want to deal with Shadow DOM polyfills, and for (maybe) the last time: Shadow DOM is not needed to create, or deliver, custom elements!
- however, if you want a non standard Shadow DOM, you don't need more than 1.4K to have it down to IE9
- but if you target mobile, or even less than IE 11, you're way better off with my polyfill, the one used in these years by Google AMP or AFrame. No, it doesn't provide Shadow DOM, because it's a different specification/part of the Web Components umbrella.
- if you target evergreen browsers, all you need to go full steam is feature detect Safari and load the right poly
When you have some Web standards advocate with NIH syndrome, it's difficult to get a spot in other articles and projects.
However, you'll find the other polyfill links literally everywhere else, so I'll reserve this gist for my contribution, 'hope you don't mind.
I am not sure what is the issue presented in there, 'cause I don't see how any other non live solution would work differently, whatever content supposed to land on that toggle/slot.
If the difference is that some automation might place DOM nodes live only on toggles, the same automation could do exactly what it does with custom elements too.
Moreover, in my real-world experience with Custom Elements, composing is exactly what makes CE amazing!
Regardless, do you know what's the issue with that element? Its attributeChangedCallback doesn't check if the node itself is live, and visible, or not, which is nothing that terrible to eventually solve, even on user-land:
<details ontoggle="
var hi = this.querySelector('html-include');
if (!hi.src)
hi.src = hi.dataset.src;
">
<summary>Toggle the section for more info:</summary>
<toggled-section>
<html-include data-src="./more-info.html"/>
</toggled-section>
</details>
Beside the "Oh My Gosh, JS in HTML" heresy, written just to keep it simple, you can see having to deal with Custom Elements is really not too different from dealing with any other node.
The presented solution is usable with script, style, or even images, all things that might prematurely load content right away, so feel free to reuse the technique.
Yes, this might be annoying. I have created a list of handy custom elements patterns, and admittedly it's repetitive and boring to deal with this dual attributes nature.
Truth to be told, the platform gave us, after us asking, a place to pass down props automatically reflected as attributes, and is the data-
attribute.
'cause if you fallback accessors to get/setAttribute
calls, you are better off dealing with dataset
.
class MyThing extends HTMLElement {
static get observedAttributes() {
return ['data-foo', 'data-bar', 'data-baz'];
}
attributeChangedCallback(name, oldValue, newValue) {
name = name.slice(5);
if (name === 'foo') {
// ...
}
if (name === 'bar') {
// ...
}
if (name === 'baz') {
// ...
}
}
}
That's it, any relevant dataset
operation will be reflected on the node: this.datased.foo = "bar"
, not a big deal.
Despite this solution, wouldn't a decorator also be a valid way to remove unnecessary bloat? If you transpile, transpiling also something in stage 2 shouldn't be too scary (but yeah, no idea what's going on with that proposal).
@observedAttributesAccessor
class MyThing extends HTMLElement {
static get observedAttributes() {
return ['foo', 'bar', 'baz'];
}
}
I agree the *Callback
method convention is super ugly, and it doesn't work as a guard for the method.
However, I've been using attributechanged utility to have attributechanged
event instead, but it doesn't look like developers were really looking for that ... also 'cause a manual dispatch is side-effects prone as much as invoking the method directly?
I don't know, but you like events more, like I do, I can suggest also to have a look at disconnected, which brings onconnected
and ondisconnected
events to any DOM element, including Custom Elements.
These two are part of the core of hyperHTML, and lighterhtml-plus, while at least for boolean values, HyperHTMLElement offers a booleanAttributes
extra getter.
Last, but not least, 🔥 heresy 🔥 backes almost the best of all these libraries, providing events out of the box, and some other utility borrowed here and there.
I might be too old here, but I think the DOM these days is surely still not immediate to grasp, but awesome enough to support the explosion of libraries, utilities, and frameworks on top of it, which are all different, and mostly blazing fast, despite all those indirections/
Classifying the DOM as categorically bad doesn't really seem fair.
What I think is also unfair, are developers not much into Web Components or even Custom Elements, that keep talking about Web Components as an "all-or-nothing" thing, without analyzing the potentials of every single specification of the group.
Back to the <Adder a={1} b={2}/>
example, this is how you'd do it on heresy:
import {define, render, html} from '//unpkg.com/heresy?module';
// the Adder components
const Adder = {
extends: 'div',
oninput() {
const [a, b] = this.children;
this.dataset.a = a.value;
this.dataset.b = b.value;
this.render();
},
render() {
const {a, b} = this.dataset;
this.html`
<input type="number" value=${a} oninput=${this}>
<input type="number" value=${b} oninput=${this}>
<p>${a} + ${b} = ${parseFloat(a) + parseFloat(b)}</p>
`;
}
};
// defined globally, instead of locally
define('Adder', Adder);
// and rendered on the body
render(document.body, html`<Adder data-a=${1} data-b=${2} />`);
And you can see the result live in Code Pen.
It might be not as magic as in Svelte, but it needs zero tooling and it works SSR too.
Sure comparing yet another library wasn't the point here, but all my tiny helpers do is to enhance everything already possible via the current DOM, including local custom elements per component: it's possible!
Indeed I also find the global namespace annoying, but heresy is able to cover that bit as well.
const Body = {
extends: 'body',
includes: {Adder},
render() {
this.html`<Adder data-a=${1} data-b=${2} />`;
}
};
// note: no Adder definition here, it's kept local in Body
// and Body can be used locally too elsewhere, no clashes ever
define('Body', Body);
render(document.documentElement, html`<Body />`);
This is where we indeed agree, but for a different conclusion:
- Custom Elements are already usable everywhere
- Custom Elements builtin exist natively, and are nicely supported by the right polyfills
- you don't need the complex parts of Web Components if you just need some custom element
- you don't need to simulate 1:1 JSX, but you can get pretty closer already with heresy, hyperHTML, or plain lighterhtml, which are ale developed via standards, in JavaScript, and need zero toolchain, hence their components are widely distributable in the wild, or surely never too heavy for your own project.
Please give Custom Elements a better chance, they don't deserve to be put in shame due other parts of the specs.
Thanks for reading.
Ever heard of
<template>
tag?