Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active December 5, 2023 17:47
Show Gist options
  • Save WebReflection/71aed0c811e2e88e3cd3c647213f0e6c to your computer and use it in GitHub Desktop.
Save WebReflection/71aed0c811e2e88e3cd3c647213f0e6c to your computer and use it in GitHub Desktop.
Why I use web components

Why I use web components

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.

The Web Components Umbrella

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.

1. Progressive enhancement

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)

Even Better SSR

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.

2. CSS maybe in JS

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.

3. Platform fatigue

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?

4. Polyfills

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

... why everything you link sounds like ... different?

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.

5. Composition

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.

6. Confusion between props and attributes

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'];
  }
}

7. Leaky design

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.

8. The DOM is not that bad

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.

9. Global namespace

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 />`);

10. These are all solved problems

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.

@Rich-Harris
Copy link

The main value of the <TwitterShare> component — https://svelte.dev/repl/98aa20d4cb3d40dabfef7d8dae183b85?version=3.5.2 — is that it generates the href automatically, given props like text, url and via. In your <a is="twitter" ...> example, you have to generate the href yourself, which is finicky and easy to get wrong, and defeats the object of componentisation which is to encapsulate that sort of logic. I wasn't making a point about whether or not you can extend built-in elements.

@WebReflection
Copy link
Author

Thanks for coming back. So, I've updated the first paragraph with an example that uses a custom elements and offers already some advantage (read the SSR related bit).

These kind of components are indeed ideal for composition, auto-hydration, serve from JS as well from the backend with even less bloat, and automatically bootstrap on demand.

If you'd like to detect custom elements on the fly and load both definition and related CSS you can do that too: https://github.com/WebReflection/lazytag#lazytag

@WebReflection
Copy link
Author

FYI I've updated the first paragraph providing a real example of built-in redefinition via heresy-ssr

@Buslowicz
Copy link

The main value of the <TwitterShare> component — https://svelte.dev/repl/98aa20d4cb3d40dabfef7d8dae183b85?version=3.5.2 — is that it generates the href automatically, given props like text, url and via. In your <a is="twitter" ...> example, you have to generate the href yourself, which is finicky and easy to get wrong, and defeats the object of componentisation which is to encapsulate that sort of logic. I wasn't making a point about whether or not you can extend built-in elements.

Ever heard of <template> tag?

@Rich-Harris
Copy link

@Draccoz yes! As the author of two UI frameworks, I'm extremely familiar with the DOM, including <template>. What relevance does it have here?

@Buslowicz
Copy link

Buslowicz commented Jun 24, 2019 via email

@Rich-Harris
Copy link

I know! Like I said, I'm very familiar with the workings of the DOM. How does it help solve the problem I identified?

@Buslowicz
Copy link

Bollocks, mistaken that with point 5 of your blog post (been very tired when I read that, though that's not an excuse).
But to the point 5 then, what's wrong with <template> in the context of point 5 (composition)?

As to point 1 and progressive enhancement, no JS framework works in the browser without JS. If you count SSR, it is possible with web components as well, just need to plan the component architecture ahead and do it carefully (Shadow DOM does not need to be used everywhere).

That's to touch just 2 points of entire blog post, but if you are willing to reply, I would like to ask more questions.

@WebReflection
Copy link
Author

@Draccoz I think Rich has few points, but I wanted to counter argument few others here. It's still true you very often need some little helper to fully enjoy the platform, but as long as misconceptions are clarified, in this case about Custom Elements, anyone is obviously free to pick the tool they like the most.

Please let's keep this gist just conversational, 'cause all I think I can do is just delete this gist, which'd be unfortunate. Thanks.

@Buslowicz
Copy link

@WebReflection I am truly thankful for your input in the Custom Elements field and for creating this gist. By no means I would want this to become a spam or place for off topic talk.
However, as this is a direct response to the Rich's recent post, I consider it helpful to have answers to basic fundamentals here as well. I will not ask further questions here if you consider them not helpful, but would ask you to allow for just finishing the context of compositions. If you prefer to cut this chat, please let me know and I will stop it here and probably create a small gist with questions from myself hoping @Rich-Harris will answer them there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment