UI Development

Web Components

 

Introduction

Web components are a set of different technologies that allows us to create reusable and encapsulated HTML tags with their functionality away from the rest of the code. Using web components developers are no longer limited to the existing HTML tags that the browser vendors provide.

Web components provides us with many benefits like reusability, readability, maintainability, consistency and interoperability.

Concepts and Usage

Web components consists of three main technologies.

  1. Custom Elements
  2. shadow DOM
  3. HTML Templates and Slots

Custom Elements

Custom Elements are the major building blocks of Web Components. Custom Elements are a set of different APIs that enables developers to extend HTML elements, build new ones and define their behavior. Custom elements enables you to create custom HTML tags which can exhibit any JavaScript behavior.

There are 3 rules that we need to follow while creating a custom element.

  1. You cannot register the same name for Custom Element tag more than once.
  2. Web Components tgs cannot be self closing.
  3. The name of a Web Component needs to contain a hypen (-).

Custom elements contain their own meaning, behaviors, markup and can be shared across frameworks and browsers.

Javascript Class:

class MyComponent extends HTMLElement {
    connectedCallback() {
        this.innerHTML = `<h1>Web Components</h1>`;
    }
}

Registering the component using define()

customElements.define('my-component', MyComponent);

Adding the component to HMTL

<my-component></my-component>

CustomElementRegistry is the controller of custom elements and allows you to register a custom element on the page. Custom Element can be defined like:

customElements.define('my-component', MyComponent, { extends: 'p' });

where ‘my-component’ refers to the name of the element, MyComponent is a class object that defines the behavior of the element and optionally, an options object containing an extends property, which specifies the already built-in element, your custom element inherits. Here it extends the <p> element.

A custom element’s class object is written using standard ES class syntax. For example, MyComponents structured like so:

class MyComponent extends HTMLParagraphElement {
    constructor() {
        // Always call super first in constructor
        super();
        //functionality is written here
    }
}

There are two types of custom elements:

  1. Autonomous custom elements
  2. Customized built-in elements

Autonomous custom elements

Autonomous custom elements nearly always extend HTMLElement.

class info extends HTMLElement {
    constructor() {
        super(); //call parent constructor and add context, access and attributes
        //write the remain functionality here
    }
}
 constructor() definition always starts by calling super() so that the correct prototype chain is established.

Inside the constructor, we have all the functionality the element will have when an instance of it is created. Here we attach a shadow root to the custom element, use some DOM manipulation to create the element’s internal shadow DOM structure which is then attached to the shadow root.

var shadow = this.attachShadow({mode: 'open'});
// Create span
var info = document.createElement('span');
info.setAttribute('class','info');
var text = this.getAttribute('text');
info.textContent = text;
shadow.appendChild(info);

Then, we register our custom element on the CustomElementRegistry using the define().

customElements.define('text-info', Info);

It is now available to use .We use it in our HTML like:

<text-info text="Your card validation code (CVC) is an extra security feature — it is the last 3 or 4 numbers on the
back of your card."></text-info>

Customized built-in elements

Customized built in element example — expanding-list. It is like the expanding/collapsing menu.

We define our element’s class:

class ExpandingList extends HTMLUListElement {
    constructor() {
        super();
        // write element functionality in here
    }
}

The difference here is that cutom built in element is extending the HTMLUListElement interface, and not the HTMLElement. So it has all the characteristics of a <ul> element with the functionality we define built on top, rather than being a standalone element. This difference makes it a customized built-in element.

We register the element using the define() method, but here it also includes an options object that details what element our custom element inherits from:

customElements.define('expanding-list', ExpandingList, { extends: "ul" });

Using the built-in element in a document  looks different:

<ul is="expanding-list">
 
...
 
</ul>

You use a <ul> element as normal, but specify the name of the custom element inside the is attribute.

Using the lifecycle callbacks

We can define different callbacks methods inside a custom element’s class definition, which fire at different points in the element’s lifecycle:

  1. connectedCallback: Invoked each time the custom element is appended into a DOM element and each time the node is moved
  2. disconnectedCallback: Invoked each time the custom element is removed or disconnected from the DOM.
  3. adoptedCallback: Invoked every time the custom element is moved to a new document.
  4. attributeChangedCallback: Invoked each time one of the custom element’s attributes is modified like whether any attribute is added, removed, or changed. (Changed attribtes are specified in a static get observedAttributes method)
Example.

This is an example that simply generates a colored square of a fixed size on the page. The custom element looks like this:

<custom-square l="100" c="red"></custom-square>

In the constructor class, we attach a shadow DOM to the element, then attach empty <div> and <style> elements to the shadow root:

var shadow = this.attachShadow({mode: 'open'});
var div = document.createElement('div');
var style = document.createElement('style');
shadow.appendChild(style);
shadow.appendChild(div);
The important method in this example is updateStyle() . This function takes an element, gets its shadow root, finds its <style> element, and adds width, height, and background-color to the style.
function updateStyle(elem) {
    const shadow = elem.shadowRoot;
    shadow.querySelector('style').textContent = `
        div {
            width: ${elem.getAttribute('l')}px;
            height: ${elem.getAttribute('l')}px;
            background-color: ${elem.getAttribute('c')};
        }
    `;
}
The updates are all handled by the life cycle callbacks placed inside the class definition. The connectedCallback() runs each time the element is added to the DOM  and so here we run the updateStyle method to update the styles of the element.
connectedCallback() {
    console.log('Custom square element added to page.');
    updateStyle(this);
}
The disconnectedCallback() and adoptedCallback() callbacks here just are used to inform us if an element is removed or moved to a different page.
disconnectedCallback() {
    console.log('Custom square element removed from page.');
}
adoptedCallback() {
    console.log('Custom square element moved to a new page.');
}
The attributeChangedCallback() callback is run whenever one of the attributes is modified. We are running the updateStyle() function again to make sure that the square’s style is updated.
attributeChangedCallback(name, oldValue, newValue) {
    console.log('Custom square element attributes changed.');
    updateStyle(this);
}

To get the attributeChangedCallback() callback method  to fire when an attribute changes, you have to observe the attributes. We get the array containing the names of the atrributes by specifying a static get observedAttributes() method inside custom element class.

static get observedAttributes() { return ['c', 'l']; }

Shadow DOM

The Shadow DOM API provides a way to attach a hidden separated DOM to an element and enables us to keep the markup structure, style, and behavior hidden and separate from the other code on the web page.

The Shadow DOM is just like other DOMs that browsers can generate from HTML code, but the difference between those is the way they are being generated and how they are being used and behave with other elements on a web page. The code inside a shadow DOM cannot affect anything outside it.

Shadow DOM allows hidden DOM trees that can be attached to elements in the regular DOM tree. The shadow DOM tree starts with a shadow root, which can be attached to any elements you want.

For Example:
const button = document.createElement('button'); // create a brand new button element
button.innerHTML = 'Click me'; // give that poor button a label
document.querySelector('body').appendChild(button); // append the button to the document's body

The DOM Tree will look like this:

html
└─ head
│ └─ title
│ └─ Web Components ftw!
└─ body
└─ h1
└─ Let’s learn about Shadow DOM!
└─ button
└─ Click me

Below are the shadow DOM terminology, we must be aware of:

  1. Shadow host: The DOM node to which the shadow DOM is attached.
  2. Shadow tree: The DOM tree which is present inside the shadow DOM.
  3. Shadow boundary: The place where the shadow DOM ends, and the regular DOM begins.
  4. Shadow root: The root node of the shadow tree.

Usage

Shadow root can be attached to any element using the Element.attachShadow() method. Options object is the parameter that contains one option — mode — with a value of open or closed:

let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});
 Open means that we can access the shadow DOM using JavaScript written in the main page and for closed set, you won’t be able to access the shadow DOM from the outside.

If we want to attach a shadow DOM to a custom element, we would use something like this:

let shadow = this.attachShadow({mode: 'open'});

When shadow DOM is attached to an element, we can manipulate it using the same DOM APIs as we use for the regular DOM manipulation:

var para = document.createElement('p');
shadow.appendChild(para);
 Example:

Here we take an image icon and a text string, and embeds the icon into the page. When the focus is on the icon, it displays the text in a pop-up box. In a JS file, we define a class PopUpInfo, which extends HTMLElement:

class PopUpInfo extends HTMLElement {
    constructor() {
        super();
        // write element functionality in here
    }
}
 Then attach a shadow root to the custom element.
// Create a shadow root
var shadow = this.attachShadow({mode: 'open'});
//DOM manipulation to create the shadow DOM structure:
 
// Create spans
var wrapper = document.createElement('span');
wrapper.setAttribute('class','wrapper');
var icon = document.createElement('span');
icon.setAttribute('class','icon');
icon.setAttribute('tabindex', 0);
var info = document.createElement('span');
info.setAttribute('class','info');
 
// Take text content and put it inside the info span
var text = this.getAttribute('text');
info.textContent = text;
 
// Insert icon
var imgUrl;
if(this.hasAttribute('img')) {
    imgUrl = this.getAttribute('img');
} else {
    imgUrl = 'img/default.png';
}
var img = document.createElement('img');
img.src = imgUrl;
icon.appendChild(img);
 After that we create a <style> element and populate it with some CSS to style it:
var style = document.createElement('style');
 
style.textContent = `
.wrapper {
    position: relative;
}
 
.info {
    font-size: 0.8rem;
    width: 200px;
    display: inline-block;
    border: 1px solid black;
    padding: 10px;
    background: white;
    border-radius: 10px;
    opacity: 0;
    transition: 0.6s all;
    position: absolute;
    bottom: 20px;
    left: 10px;
    z-index: 3;
}
 
img {
    width: 1.2rem;
}
 
.icon:hover + .info, .icon:focus + .info {
    opacity: 1;
}`;
 The last step is to attach all the created elements to the shadow root:
shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);

// Define the new element

customElements.define('popup-info', PopUpInfo);

This is how we add it in HTML.

<popup-info img="img/alt.png" text="Your card validation code (CVC) is an extra security feature — it is the last 3 or 4numbers on the back of your card.">

 

HTML Templates and Slots

<template> and <slot> are the elements, which are used to create a flexible template that can be used to generate the shadow DOM of a web component.

When we have to reuse the same markup on a web page, we can use a template rather than repeating the same structure over and over again in the HTML. This can be acheived using HTML <template> element. Template and its contents are not rendered in the DOM, but it can still be referenced using JavaScript.

Example:

<template id="my-paragraph">
    <style>
        p {
            color: white;
            background-color: #666;
            padding: 5px;
        }
    </style>
    <p>My paragraph</p>
</template>
 To output the template content , we have to create a reference with Javascript and then append it to the DOM.
customElements.define('my-paragraph',
    class extends HTMLElement {
        constructor() {
            super();
            let template = document.getElementById('my-paragraph');
            let templateContent = template.content;
            const shadowRoot = this.attachShadow({mode: 'open'})
            .appendChild(templateContent);
        }
    }
);
 We can add it to our HTML document like:
<my-paragraph></my-paragraph>

Adding flexibility with slots

Using templates, we can only display text inside it. It is possible to display different text in each element instance using the <slot> element. Slots are identified by their name, and allows you to define placeholders in your template that can be filled with any markup.

To add a slot into our example, we could update our template’s element like this:

<p><slot name="my-text">Default text</slot></p>

If the browser doesn’t support slots or if the slot’s content is not defined when the element is included in the markup, <my-paragraph> just contains the fallback content “My default text”.

To define the slot’s content, we include an HTML structure inside the <my-paragraph> element with a slot attribute whose value is equal to the name of the slot we want it to fill. For example:

<my-paragraph>
    <span slot="my-text">Different text!</span>
</my-paragraph>
 Below is an example to show how to use <slot> together with <template>,

Create a <element-details> element with named slots in its shadow root and design the <element-details> element in a way that, when used in documents, it is rendered from composing the element’s content together with content from its shadow root.

First, we use the <slot> element within a <template>, to create a new “element-details-template” document fragment containing some named slots:

<template id="element-details-template">
    <style>
        details {font-family: "Open Sans Light",Helvetica,Arial}
        .name {font-weight: 500; color: #000000;}
        h4 { margin: 8px 0 -5px 0; }
        h4 span { background: #000000; padding: 2px 7px 2px 7px }
        h4 span { border: 1px solid #cee9f9; border-radius: 5px }
        h4 span { color: #ffffff }
       .attributes { margin-left: 20px; }
       .attributes p { margin-left: 15px; font-style: italic }
    </style>
    <details>
        <summary>
            <span>
                <code class="name">&lt;<slot name="element-name">NEED NAME</slot>&gt;</code>
                <i class="desc"><slot name="description">NEED DESCRIPTION</slot></i>
            </span>
        </summary>
        <div class="attributes">
            <h4><span>Attributes</span></h4>
            <slot name="attributes"><p>None</p></slot>
        </div>
    </details>
</template>

That <template> element has several features like <style> element with a set of CSS styles and the <template> uses <slot> and its name attribute to make three named slots:

<slot name=”element-name”>
<slot name=”description”>
<slot name=”attributes”>

The <template> wraps the named slots in a <details> element. Creating a new <element-details> element from the <template>.

Next, create a new custom element <element-details> and use Element.attachShadow to attach to it.

customElements.define('element-details',
    class extends HTMLElement {
        constructor() {
            super();
            var template = document.getElementById('element-details-template').content;
            const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(template.cloneNode(true));
        }
    }
);

Using the <element-details> custom element with named slots in our document,

<element-details>
    <span slot="element-name">slot</span>
    <span slot="description">A placeholder</span>
    <dl slot="attributes">
        <dt>name</dt>
        <dd>The name of the slot.</dd>
    </dl>
</element-details>
 
<element-details>
    <span slot="element-name">template</span>
    <span slot="description">Holding client-side content</span>
</element-details>

The above code snippet has two instances of <element-details> and both use the slot attribute to reference the named slots “element-name” and “description” in the <element-details> shadow root .

The first <element-details> element references the “attributes” slot using a <dl> element with <dt> and <dd> children. The second <element-details> element doesn’t have any reference to the “attributes” named slot.

Add more CSS for the <dl>, <dt>, and <dd> elements in our doc:

dl { margin-left: 10px; }
dt { font-weight: normal; color: #000000;}
dd { margin-left: 15px }

Below is the Result.

Web components Vs React Components

There is a misunderstanding that we often think Web Components and React are the same. The truth is we can use them for a similar result, but their primary use is different.

Web Components

The goal behind using Web Components is to provide a safe environment to create components with readability, encapsulation, and isolation. Web Components uses three main technologies — Custom Elements, Shadow DOM, HTML Templates, but the important ones that most people use are Custom Elements and Shadow DOM.

React

React’s primary use is to provide UI components that can render as fast as possible. It’s primary focus is speed and performance. There are other important reasons to use React. It includes: better development experience, debugging, and it’s simple.

Conclusion

Both are excellent technologies, but focus should be on the main for what they were created. Here we see that encapsulation and performance are the areas where these two technologies diverge. We can use React inside Web Components to speed up the rendering, and use Web Components inside React to simplify and provide clean encapsulation and isolation of custom elements.

About The Author