Web Components, an Overview
Tags: webdev, webcomponents, customelements, shadowdom
Published: 2020-12-01 05:41:09

Web Components are a selection of technologies that together can be used to bring us a lot of the benefits of custom component and rendering libraries but natively on the web.  Currently, there are 3 main pillars:

* HTML Templates
* Shadow DOM
* Custom Element

HTML Templates are a way to add inert HTML to the DOM that you can clone.  Shadow DOM adds encapsulation for styles and DOM so that they are treated as internal to an element.  Custom Elements are a way to register custom tags that can have user-defined behavior.

## Templates
Templates are fairly self-explanatory, first you write the HTML:

```html
<template id="foo">
  <div>Hello World</div>
</template>
```
Then you can use it like this:

```js
const div = document.getElementById("foo").content.cloneNode(true);
document.body.appendChild(div);
```
The template HTML is not rendered but it does get parsed into DOM nodes. It stays inert and you can use the `.content` property to get a reference to it's inner content as a `documentFragment`.  Typically, this is combined with `cloneNode(true)` (with the `true` indicating a deep subtree copy) so you can reuse the DOM nodes without the overhead of having to parse HTML from strings.

## Custom Elements
Custom elements allow you to register tag names that can generate custom HTML elements.  Note that these are specifically called "custom elements" but it's really easy to get confused or habitually call them "web components" even though that's not accurate.

Here's perhaps the simplest of custom elements:

```js
customElements.define("my-element", class extends HTMLElement {
  connectedCallback(){
    this.innerHTML = `<div>Hello World!</div>`
  }
});
```
Assuming this script is registered with the DOM, we can use it like this:

```html
<my-element></my-element>
```

Custom elements are classes that a registered with `customElement.define`.  Note that the class must extend `HTMLElement`.  Some browsers will let you extend other elements for better accessibility but Safari unfortunately does not allow this.  They can optionally have methods for a few lifecycle methods`

* `connectedCallback` when the element is attached to the DOM
* `adoptedCallback` when the element is moved to a new DOM (you likely won't use this one)
* `disconnectedCallback` when the element is removed from the DOM
* `attributeChangedCallback` when the elements "observed" (more on this later) attributes change.

`connectedCallback` is probably the most interesting as that's what's going to kick off all of your code.  One special thing to note is that tag names must contain a hyphen.  This is primitive namespacing mechanism so HTML can continue to add new non-hyphenated elements without breaking existing code.

## Shadow DOM

Shadow DOM is perhaps the most complicated pillar as it fundamentally changes how DOM works by creating a separate DOM tree for elements that has very limited interaction with the main DOM.

Using the example above, here's how we would produce it:


`````js
customElements.define("my-shadow-element", class {
  connectedCallback(){
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = "<div id='x'>Hello From the Shadow World!</div>";
  }
});
```
(excuse the extra tick, it's a markdown parsing bug)

`attachShadow` adds a shadow root to the element and there can be only one. If we were to run `querySelector("#x")` on the document, the inner div would not be returned as it does not exist in the light DOM.  It is still possible to query it from the element directly `querySelector("my-shadow-element").shadowRoot.querySelector("#x")` though.  However, even this can be disabled using `attachShadow({mode: "closed"})` which will cause `.shadowRoot` to always return null.  Under these circumstances only the reference returned from `attachShadow` can be used to access it, and if you throw it away nothing can access that shadow DOM anymore.

So why would you want this?  Shadow DOM is used for DOM encapsulation, you can have guarantees that other scripts cannot add to or modify the DOM as they don't have references.  Shadow DOM also fully encapsulates CSS.  Outer CSS cannot get in, except in highly specific ways, and styles used inside the element cannot get out.  This allows you to build components where the internals can be be kept internal, the same way if you use a `<video>` element the controls to play, stop, change volume and navigate are not directly accessible to outside code, they're just part of the element itself.  Shadow DOM can also define slots in which child content can be added from the outside to specific locations.  This is done by using the `slot` tag:

```js
customElements.define("my-shadow-element", class {
  connectedCallback(){
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
     <div id='x'>Hello From the Shadow World!</div>
     <slot name="content"></slot>`;
  }
});
```
Outside element are matched to slots by name so:

```html
<my-shadow-element>
  <div slot="content">Outside</div>
</my-shadow-element>
```
Will be slotted into the content slot.  There is ongoing work to make this work via index as well.

## HTML Imports?

If you look at earlier specs and articles you might find a thing called HTML imports.  This was a proposed way into which to import other HTML content (scripts, templates, styles) from other HTML documents via a link tag.  It was a very interesting idea that never really took off.  Since this was going on at the same time ESM was being spec'd out, it was hard to align and eventually dropped.  Now the expectation is that we'll use imports for everything (and if you use some build tools you already do!).  This doesn't exist in browsers yet though.  This will be cool for JS but it's a bit sad there is no plan for HTML to get similar powers.

Hopefully that gives a brief overview of the landscape.  It's not too complicated but work is ongoing and new features are added constantly to each pillar.

## Practical Application

While this is just a 10,000 foot view of the landscape, if you are looking for more practical application, including API design and how-tos of real world components, or just some inspiration I've started a series on building components completely from scratch, just JS, HTML and CSS: https://dev.to/ndesmic/series/9830