published
updated
https://developer.mozilla.org/en-US/docs/Web/API/Web_components
Web Components are a set of web technologies that make defining and re-using custom HTML elements easy. According to MDN there's three pillars upon which the standard makes this possible:
- Custom Elements
- Shadow DOM
<template>
and<slot>
elements
Custom Elements
Custom Elements are handled by the CustomElementRegistry
object. The name is pretty self explanatory, it's a registry of your custom elements.
To create a custom element you use CustomElementRegistry.define()
which takes a kebab-case string as the name, a Javascript Class which defines functionality, and optionally an HTML element that it extends.
{{{javascript class WordCount extends HTMLParagraphElement { constructor() { // Always call super first in constructor super();
const wcParent = this.parentNode;
function countWords(node){
const text = node.innerText || node.textContent;
return text.trim().split(/\s+/g).filter(a => a.trim().length > 0).length;
}
const count = `Words: ${countWords(wcParent)}`;
// Create a shadow root
const shadow = this.attachShadow({mode: 'open'});
// Create text node and add word count to it
const text = document.createElement('span');
text.textContent = count;
// Append it to the shadow root
shadow.appendChild(text);
// Update count when element content changes
setInterval(function() {
const count = `Words: ${countWords(wcParent)}`;
text.textContent = count;
}, 200);
} }
customElements.define("word-count", WordCount, { extends: "p" }); }}}
The above is a simple example. It's also possible to define specific callbacks for the class, that get executed at specific points in the elements lifecycle.
=### Callbacks =
There are a few callbacks that can be defined for custom elements:
connectedCallback
- Invoked every time the element is added into a document connected element
- May also be called before the element has been parsed
- May also be called once your element is no longer connected (wtf?) You can use
Node.isConnected
to make sure
disconnectedCallback
- Invoked every time the element is disconnected from the document's DOM
adoptedCallback
- Invoked every time the node is moved to a new document
attributeChangedCallback
- Invoked each time one of element's attributes is added, removed or changed.
Shadow DOM
The shadow DOM is a way to enable encapsulation, allowing code to not "clash" with other custom elements, by attaching elements to a separate DOM. The shadow DOM allows elements to be attached to the regular DOM, and starts with a "shadow root" under which any elements can be attached just like the regular DOM The Shadow DOM API allows you to manipulate any Shadow DOM you attach to your custom elements
Some terminology:
- Shadow Host
- The regular DOM node that the shadow DOM attaches to
- Shadow Tree
- The DOM tree inside the Shadow DOM
- Shadow Boundary
- The place where the shadow DOM meets the regular DOM
- Shadow Root
- The root node of the shadow tree
=### Basic Usage =
The Element.attachShadow()
allows you to attach a shadow root to any element. It takes as a parameter an object with mode
which is either open
or closed
, referring to whether you can refer to the shadow root with Javascript from the "outside". Built-in HTML elements that use a Shadow DOM (such as <video>
) use a closed shadow DOM, meaning that element.shadowRoot
return null
An open shadow DOM has the same API as the regular DOM
{{{javascript Element.attachShadow({ "mode": "open" }) Element.attachShadow({ "mode": "closed" }) }}}
Templates and Slots
As the name kind of implies, the template tag allows creating content templates. If you have a particular set of HTML that you plan to use a bunch, it makes sense to turn it into a template. However the template tag by itself isn't very flexible. It copies exactly what's in the template. This is where slot tags come in. You can define slots in templates which you can then override each time you create an instance of your template. It's a fairly simple idea, with lots of malleability.
Let's look at an example
{{{javascript
<code class="name"
><<slot name="element-name">NEED NAME</slot>></code
>
<span class="desc"
><slot name="description">NEED DESCRIPTION</slot></span
>
</span>
</summary>
<div class="attributes">
<h4><span>Attributes</span></h4>
<slot name="attributes"><p>None</p></slot>
</div>
</details>
</template> }}}
Here we have a template that contains 2 slots, an "element-name" and "description". From this template we can create a custom element like so:
{{{javascript customElements.define( "element-details", class extends HTMLElement { constructor() { super(); const template = document.getElementById( "element-details-template" ).content; const shadowRoot = this.attachShadow({ mode: "open" }); shadowRoot.appendChild(template.cloneNode(true)); } } ); }}}
Note, we're defining our class in the parameter of customElement.define()
so we don't need to give it a name. Now that we have a custom element based on this template, when we use it in our HTML, we can pass in the details to that specific element by overwriting the slot like so:
{{{javascript
>A placeholder inside a web component that users can fill with their own
markup, with the effect of composing different DOM trees together.</span
>
- name
- The name of the slot.
>A mechanism for holding client- side content that is not to be rendered
when a page is loaded but may subsequently be instantiated during runtime
using JavaScript.</span
> </element-details> }}}
We're specifically using named slots to reference the data to put in each slot. Since we created a shadow DOM for this component, the style doesn't interfere with the rest of the page. Also, not all the elements we define reference all aspects of the template. The attributes slot is only referenced by the first.