Web components - 01

revision:


Content

intro concepts and usage custom elements shadow DOM HTML templates Lifecycle hooks creating HTML templates - examples creating a custom element - examples


intro

top

Web Components is a suite of different technologies allowing you to create reusable custom elements (with their functionality encapsulated away from the rest of your code) and utilize them in your web apps.

Using a web component is much like using any other existing HTML element.
They can be configured using attributes, queried for using JavaScript, and even styled through CSS.
As long as the browser knows they exist, they are treated no differently.


concepts and usage

top

Web Components consists of three main technologies, which can be used together to create versatile custom elements with "encapsulated functionality" that can be reused wherever you like without fear of code collisions.

Custom elements: a set of JavaScript APIs that allow you to define custom elements and their behavior.

Custom elements are HTML elements (like <div>, <section>or <article>). We can name them ourselves and they are defined via a browser API.
Custom elements are just like standard HTML elements (i.e. names in angle brackets), except that they always have a dash in them, like <news-slider> or <bacon-cheeseburger>. Browser vendors have committed not to create new built-in elements containing a dash in their names to prevent conflicts.
Custom elements contain their own semantics, behaviors, markup and can be shared across frameworks and browsers.

Shadow DOM: a set of JavaScript APIs for attaching an encapsulated "shadow" DOM tree to an element — which is rendered separately from the main document DOM — and controlling associated functionality.

In this way, you can keep an element's features private, so they can be scripted and styled without the fear of collision with other parts of the document.
The shadow DOM is an encapsulated version of the DOM. This allows authors to effectively isolate DOM fragments from one another, including anything that could be used as a CSS selector and the styles associated with them.
Generally, any content inside of the document's scope is referred to as the light DOM, and anything inside a shadow root is referred to as the shadow DOM.
The shadow DOM works sort of like an <iframe>where the content is cut off from the rest of the document; however, when we create a shadow root, we still have total control over that part of our page, but scoped to a context. This is what we call encapsulation.

HTML templates: the ><template> element and <slot> element enable you to write markup templates that are not displayed in the rendered page.

These can then be reused multiple times as the basis of a custom element's structure. The HTML <template> element allows us to stamp out re-usable templates of code inside a normal HTML flow that won't be immediately rendered, but can be used at a later time.

examples

Choose template
code:
                 <div>
                    <template style="margin-left: 3vw;" id="book-template">
                        <li><span class="title"></span> — <span class="author"></span></li>
                    </template>
                    <template style="margin-left: 3vw;"id="book-template-2">
                        <li><span class="author"></span>'s classic novel <span class="title"></span></li>
                    </template>
                    <ul style="margin-left: 3vw;" id="books"></ul>
                    <fieldset style="margin-left: 3vw;" id="templates">
                        <legend>Choose template</legend>
                        <label>
                            <input type="radio" name="template" value="book-template" checked> Template One
                        </label>
                        <label>
                            <input type="radio" name="template" value="book-template-2"> Template Two
                        </label>
                    </fieldset>
                </div>
                <style>
                    label {display: block; margin-bottom: 0.5vw;}
                </style>
                <script>
                    'use strict';
        
                    const books = [
                    { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' },
                    { title: 'A Farewell to Arms', author: 'Ernest Hemingway' },
                    { title: 'Catch 22', author: 'Joseph Heller' }
                    ];
        
                    function appendBooks(templateId) {
                    const booksList = document.getElementById('books');
                    const fragment = document.getElementById(templateId);
                    // Clear out the content from the ul
                    booksList.innerHTML = '';
                    // Loop over the books and modify the given template
                    books.forEach(book => {
                        // Create an instance of the template content
                        const instance = document.importNode(fragment.content, true);
                        // Add relevant content to the template
                        instance.querySelector('.title').innerHTML = book.title;
                        instance.querySelector('.author').innerHTML = book.author;
                        // Append the instance ot the DOM
                        booksList.appendChild(instance);
                    });  
                    }
                    document.getElementById('templates').addEventListener('change', (event) => appendBooks(event.target.value));
                    appendBooks('book-template');
                </script>
            

The basic approach for implementing a web component looks like this:

1/ Create a class in which you specify your web component functionality, using the ECMAScript 2015 class syntax.

2/ Register your new custom element using the CustomElementRegistry.define() method, passing it the element name to be defined, the class or function in which its functionality is specified, and optionally, what element it inherits from.

3/ If required, attach a shadow DOM to the custom element using Element.attachShadow() method. Add child elements, event listeners, etc., to the shadow DOM using regular DOM methods.

4/ If required, define an HTML template using <template>and <slot >. Again use regular DOM methods to clone the template and attach it to your shadow DOM.

5/ Use your custom element wherever you like on your page, just like you would any regular HTML element.


custom elements

top

Each custom element has a similar structure.

They extend an existing HTMLElement class, which provides the groundwork for how an element should behave.
Inside, there are a few methods called "reactions" that are called in response to something about that element changing.
For example, "connectedCallback" will be called when the new element appears on screen.
These work similarly to the lifecycle methods found in most JavaScript frameworks. Updating the attributes on an element can change how it behaves. When an update happens, the "attributeChangedCallback" reaction will fire, which details the change. This will only happen for an attribute that is defined inside the observedAttributes array.
An element needs to be defined before the browser can do anything with it. The "define method" here takes two arguments – the tag name, and the class it should use. All tag names must contain a "-"" character to avoid any clashes with any future native elements.

The element can then be written anywhere in the page as a regular HTML tag.

Once a browser has an element defined, it then finds any of these matching tags and links up their behaviour to the class in a process known as 'upgrading'.
There are two types of custom element – 'autonomous' or 'customised built-in'.
Autonomous custom elements are not related to any existing element. Much like a <div>or <span>they do not provide any meaning to their content.
A customised built-in element – as the name implies – can enhance an existing element with new functionality. They maintain that element's normal semantic behaviours, while also being open to change. If an <input>element was customised, for example, it would still be picked up and submitted as part of a form.

The class of customised built-in component extends the class of the element it is customising.

The definition also needs to define the tag of that element through its third argument. They are also used slightly differently.
Instead of a new tag, they extend the existing tag by using the "is" attribute. The browser can read this, and upgrade them in the same way as it can an autonomous component.

CustomElementRegistry: contains functionality related to custom elements, most notably the CustomElementRegistry.define() method used to register new custom elements so they can then be used in your document.

Window.customElements: returns a reference to the CustomElementRegistry object.

Life cycle callbacks: special callback functions defined inside the custom element's class definition, which affect its behavior:
connectedCallback: invoked when the custom element is first connected to the document's DOM.
disconnectedCallback: invoked when the custom element is disconnected from the document's DOM.
adoptedCallback: invoked when the custom element is moved to a new document.
attributeChangedCallback: invoked when one of the custom element's attributes is added, removed, or changed.

Extensions for creating custom built-in elements: The "is" global HTML attribute allows you to specify that a standard HTML element should behave like a registered custom built-in element. The "is" option of the Document.createElement() method allows you to create an instance of a standard HTML element that behaves like a given registered custom built-in element.

CSS pseudo-classes: pseudo-classes relating specifically to custom elements:
:defined: matches any element that is defined, including built in elements and custom elements defined with CustomElementRegistry.define().
:host: selects the shadow host of the shadow DOM containing the CSS it is used inside.
:host(): selects the shadow host of the shadow DOM containing the CSS it is used inside (so you can select a custom element from inside its shadow DOM) — but only if the selector given as the function's parameter matches the shadow host.
:host-context(): selects the shadow host of the shadow DOM containing the CSS it is used inside (so you can select a custom element from inside its shadow DOM) — but only if the selector given as the function's parameter matches the shadow host's ancestor(s) in the place it sits inside the DOM hierarchy.

CSS pseudo-elements: pseudo-elements relating specifically to custom elements:
::part: represents any element within a shadow tree that has a matching part attribute.

Create a custom element

1/ use the "customElements.define() browser API methd and a JS class that extends the HTMLElement:

ex.

class DropDownd extends HTMLElement{
                // define behavior here
    }
    window.customElements.define('drop-down', DropDown);
    

You can also use an anonymous class:

ex.

    window.customElements.define{'drop-down', class extends HTMLElement{
                        // define behavior here

            }}
    

2/ once the custom element is defined, it can be used in a web page:

ex.

 <drop-down ></drop-down >

3/ properties can be defined on a custom element:

ex.

    class DropDown extends HTMLElement{
        // set the "fill" property
        set fill(option){
            this.setAttribute('fill', option);
        }
        // get the "fill" property
        get fill(){
             return this.Attribute('fill');
        }
    }
    

and the usage in the browser:

    <drop-down fill="true"><</drop-down>
    

4/ you can also define a constructor in the class:

ex.

    class DropDown extends HTMLElement{
        constructor(){
            super();
        }
    }
    

shadow DOM

top

A shadow DOM is created when applied to an element.

Any content can be added to the shadow DOM just like the regular – or 'light' – DOM, but it has no effect on what's happening outside of it.
Likewise, nothing in the light DOM can access the shadow DOM directly. This means we can add classes, styles and scripts anywhere in the shadow DOM without worrying about clashes.
The best use of the shadow DOM with web components comes when coupled with a custom element. By having a shadow DOM in charge of the content, any time this component is reused, its styles and structure will not affect the rest of the page.

ShadowRoot: represents the root node of a shadow DOM subtree.

DocumentOrShadowRoot: a mixin defining features that are available across document and shadow roots.

Element extensions: extensions to the Element interface related to shadow DOM: the Element.attachShadow() method attaches a shadow DOM tree to the specified element; the Element.shadowRoot property returns the shadow root attached to the specified element, or null if there is no shadow root attached.

Relevant Node additions: additions to the node interface relevant to shadow DOM: the Node.getRootNode() method returns the context object's root, which optionally includes the shadow root if it is available; the Node.isConnected property returns a boolean indicating whether or not the node is connected (directly or indirectly) to the context object, e.g. the Document object in the case of the normal DOM, or the ShadowRoot in the case of a shadow DOM

Event extensions: Extensions to the event interface related to shadow DOM: Event.composed returns a Boolean which indicates whether the event will propagate across the shadow DOM boundary into the standard DOM (true), or not (false); Event.composedPath returns the event’s path (objects on which listeners will be invoked). This does not include nodes in shadow trees if the shadow root was created with ShadowRoot.mode closed.


HTML templates

top

The HTML templates specification defines the <template>tag, which can contain anything likely to be reused.

On its own, it has no appearance and remains inert, meaning nothing inside is parsed or executed until told to, including requests for external media such as images or video. JavaScript cannot query the contents either, as browsers will only see it as an empty element.

A regular query will pick up the <template>element itself. The importNode method creates a copy of its contents, with the second argument telling it to take a deep copy of everything. Finally, it can be added to the document like any other element.
Templates can contain anything an HTML page can, including CSS and JavaScript. As soon as the element is applied to the page, those styles apply and the scripts execute.
Bear in mind that these will run globally and so can override styles and values if care isn't taken. The best part about templates is that they are not just limited to web components.
The examples here apply to any web page, but become particularly powerful when paired with web components, in particular the shadow DOM.

<template >: contains an HTML fragment that is not rendered when a containing document is initially loaded, but can be displayed at runtime using JavaScript, mainly used as the basis of custom element structures. The associated DOM interface is HTMLTemplateElement.

<slot >: a placeholder inside a web component that you can fill with your own markup, which lets you create separate DOM trees and present them together. The associated DOM interface is HTMLSlotElement.

The slot global HTML attribute: assigns a slot in a shadow DOM shadow tree to an element.

Slotable: a mixin implemented by both Element and Text nodes, defining features that allow them to become the contents of an <slot>element. The mixin defines one attribute, "Slotable.assignedSlot", which returns a reference to the slot the node is inserted in.

Element extensions: extensions to the Element interface related to slots: Element.slot returns the name of the shadow DOM slot attached to the element.

CSS pseudo-elements:Pseudo-elements relating specifically to slots: ::slotted matches any content that is inserted into a slot.

The slotchange event: fired on an HTMLSlotElement instance (<slot>element) when the node(s) contained in that slot changes.


Lifecycle hooks

top

Web components have their own lifecycle.

The following events happen in a web component’s lifecycle: element is inserted into the DOM; updates when UI event is being triggered; element deleted from the DOM.
A web component has lifecycle hooks, which are callback functions, to capture these lifecycle events and let us handle them accordingly.
They let us handle these events without creating our own system to do so.
Most JavaScript frameworks provide the same functionality, but web components is a standard so we don’t need to load extra code to be able to use them.

The following lifecycle hooks are in a web component: constructor(); connectedCallback(); disconnectedCallback(); attributeChangedCallback(name, oldValue, newValue); adoptedCallback()

The constructor() is called when the web component is created. It’s called when we create the shadow DOM and it’s used for setting up listeners and initialize a component’s state. However, it’s not recommended that we run things like rendering and fetching resources here. The connectedCallback is better for these kinds of tasks. Defining a constructor is optional for ES6 classes, but an empty one will be created when it’s undefined.
When creating the constructor, we’ve to call "super()"" to call the class that the web component class extends. We can have "return" statements in there and we can’t use "document.write()" or "document.open()" in there. Also, we can’t gain attributes or children in the constructor method.

The connectedCallback() method is called when an element is added to the DOM. We can be sure that the element is available to the DOM when this method is called. This means that we can safely set attributes, fetch resources, run set up code or render templates.

disconnectedCallback() is called when the element is removed from the DOM. Therefore, it’s an ideal place to add cleanup logic and to free up resources. We can also use this callback to: 1/ notify another part of an application that the element is removed from the DOM; 2/ free resources that won’t be garbage collected automatically like unsubscribing from DOM events, stop interval timers, or unregister all registered callbacks. This hook is never called when the user closes the tab and it can be triggered more than once during its lifetime.

attributeChangedCallback(attrName, oldVal, newVal): we can pass attributes with values to a Web Component like any other attribute. In this callback, we can get the value of the attributes as they’re assigned in the code. We can add a "static get observedAttributes()" hook to define what attribute values we observe.

The adoptedCallback() is called when we call "document.adoptNode" with the element passed in. It only occurs when we deal with iframes. The "adoptNode" method is used to transfer a node from one document to another. An iframe has another document, so it’s possible to call this with iframe’s document object


creating HTML templates - examples

top

The versatility of template: one of the interesting things about templates is that they can contain any HTML. That includes script and style elements. A very simple example would be a template that appends a button that alerts us when it is clicked.

Example 2:

code:
                <template style="margin-left: 2vw;" id="template">
                    <script>
                    const button = document.getElementById('click-me');
                    button.addEventListener('click', event => alert(event));
                    </script>
                    <style>
                    #click-me {all: unset; background: tomato;  border: 0; border-radius: 4px; color: white;  font-family: Helvetica; font-size: 1.5vw; padding: .5vw 1vw;}
                    </style>
                    <button style="margin-left:5vw;" id="click-me">Log click event</button>
                </template>
                <script>
                    'use strict';
                    const template = document.getElementById('template');
                    document.body.appendChild(document.importNode(template.content, true));
                </script>
                
            
Example 3:

code:
                <template style="margin-left: 2vw;" id="dialog-template">
                    <script>
                        document.getElementById('launch-dialog').addEventListener('click', () => {
                        const wrapper = document.querySelector('.wrapper');
                        const closeButton = document.querySelector('button.close');
                        const wasFocused = document.activeElement;
                        wrapper.classList.add('open');
                        closeButton.focus();
                        closeButton.addEventListener('click', () => {
                            wrapper.classList.remove('open');
                            wasFocused.focus();
                        });
                    });
                    </script>
                    <style>
                    .wrapper {opacity: 0; transition: visibility 0s, opacity 0.25s ease-in; }
                    .wrapper:not(.open) {visibility: hidden;}
                    .wrapper.open {align-items: center; display: flex; justify-content: center; height: 100vh;   position: fixed; top: 0; left: 0; right: 0; bottom: 0;  opacity: 1; visibility: visible;}
                    .overlay {background: rgba(0, 0, 0, 0.8); height: 100%; position: fixed; top: 0; right: 0; bottom: 0; left: 0; width: 100%; }
                    .dialog { background: #ffffff; max-width: 600px; padding: 1rem; position: fixed; }
                    button {all: unset; cursor: pointer; font-size: 1.25rem; position: absolute; top: 1rem; right: 1rem;}
                    button:focus {border: 2px solid blue;            }
                    </style>
                    <div class="wrapper">
                    <div class="overlay"></div>
                    <div class="dialog" role="dialog" aria-labelledby="title" aria-describedby="content">
                        <button class="close" aria-label="Close">✖️</button>
                        <h1 id="title">Hello world</h1>
                        <div id="content" class="content">
                        <p>This is content in the body of our modal</p>
                        </div>
                    </div>
                    </div>
                </template>
                <button style="margin-left:5vw;" id="launch-dialog">Launch dialog</button>
                <style>
                    #launch-dialog {background: tomato; border-radius: 4px; color: #fff;font-family: Helvetica, Arial, sans-serif; padding: 0.5rem 1rem; position: static;}
                </style>
                <script>
                    const template1 = document.getElementById('dialog-template');
                    document.body.appendChild(document.importNode(template1.content, true));
                </script>
            
Example 4:

code:
                <script>
                    const templateA = document.createElement('template');
                    templateA.innerHTML = `
                    <style>
                        * {font-size: 200%;}
                        span {width: 4rem; display: inline-block; text-align: center; }
                        button {width: 4rem; height: 4rem; border: none; border-radius: 10px; background-color: seagreen; color: white;}
                    </style>
                    <button id="dec">-</button>
                    <span id="count"></span>
                    <button id="inc">+</button>`;

                    class MyCounter extends HTMLElement {
                    constructor() {
                        super();
                        this.count = 0;
                        this.attachShadow({ mode: 'open' });
                    }

                    connectedCallback() {
                        this.shadowRoot.appendChild(templateA.content.cloneNode(true));
                        this.shadowRoot.getElementById('inc').onclick = () => this.inc();
                        this.shadowRoot.getElementById('dec').onclick = () => this.dec();
                        this.update(this.count);
                    }
                    inc() {this.update(++this.count);}
                    dec() {this.update(--this.count);}
                    update(count) {
                        this.shadowRoot.getElementById('count').innerHTML = count;
                    }
                    }

                    customElements.define('my-counter', MyCounter);

                </script>
            

creating a custom element - examples

top

Creating a custom element: the bread and butter of Web Components are custom elements. The customElements API gives us a path to define custom HTML tags that can be used in any document that contains the defining class. Essentially, a custom element consists of two pieces: a tag name and a class that extends the built-in HTMLElement class. The most basic version of our custom element would look like this:

    class OneDialog extends HTMLElement {
        connectedCallback() {
          this.innerHTML = `

Hello, World!

`; } } customElements.define('one-dialog', OneDialog);

Custom element lifecycle methods, custom elements have lifecycle methods, and connectedCallbackis called when an element gets added to the DOM. The connectedCallback is separate from the element’s constructor. Whereas the constructor is used to set up the bare bones of the element, the connectedCallback is typically used for adding content to the element, setting up event listeners or otherwise initializing the component. The connectedCallback is the place to modify an element.