Introduction to WebComponents with ShadowDOM
Tags: frontend, javascript, html, webcomponents
Published: 2020-02-05 16:35:59

WebComponents can be the salvation of Component-based web development.

Where all the front-end frameworks are pushing for Component approach and thinking in component style, DOM has the native way to address this. WebComponents is the collective solution to have components in the Browser natively. This collective solution includes:

* [CustomElements](https://time2hack.com/introduction-to-webcomponents-and-shadowdom/#customelements)
* [ShadowDOM](https://time2hack.com/introduction-to-webcomponents-and-shadowdom/#shadowdom)
* [HTML Template](https://time2hack.com/introduction-to-webcomponents-and-shadowdom/#html-template)
* ~~HTML Imports (Deprecated)~~

To get up and running with WebComponents, you only need the CustomElements V1 polyfill which provides a generic way to create components and lifecycle methods, which you can obtain from the following repository:

{% github webcomponents/polyfills no-readme %}

Many would say that you will need `shadowDOM`, template tags, HTML imports for your custom elements. They are right but not completely. You can create your components without them as well.

## CustomElements

CustomElements are the elements similar to native HTML elements like `div`, `span` etc. These are the extension of `HTMLElement` constructor and other similar constructors based on the type of CustomElement you wanna create.

Let's see an example; consider you wanna create a web component which will serve as a quick creation of `figure` with `img` and `figcaption` together. Normally the HTML will look like the following:

```html
<figure>
  <img
     src="https://developer.cdn.mozilla.net/media/img/mdn-logo-sm.png"
     alt="An awesome picture">
  <figcaption>MDN Logo</figcaption>
</figure>
```

The Example is taken from [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure)

And the component will look like:

```html
<img-figure
  caption="MDN Logo"
  alt="An awesome picture"
  src="https://developer.cdn.mozilla.net/media/img/mdn-logo-sm.png"
></img-figure>
```

The basic component code will be as follows:

```js
class ImgFigure extends HTMLElement {
  connectedCallback() {
    this.src = this.getAttribute("src") || null;
    this.caption = this.getAttribute("caption") || "";
    this.alt = this.getAttribute("alt") || null;
    this.render();
  }

  render() {
    this.innerHTML = this.template({
      src: this.src,
      alt: this.alt,
      caption: this.caption
    });
  }

  template(state) { 
    return `
    <figure>
      <img
        src="${state.src}"
        alt="${state.alt || state.caption}">
      <figcaption>${state.caption}</figcaption>
    </figure>
    `;
  }
}

customElements.define('img-figure', ImgFigure);
```

And its usage through JavaScript will be as follows:

```js
// create element
const i = document.createElement('img-figure');

//set the required attributes
i.setAttribute('src', '//res.cloudinary.com/time2hack/image/upload/goodbye-xmlhttprequest-ajax-with-fetch-api-demo.png');
i.setAttribute('caption', 'GoodBye XMLHttpRequest; AJAX with fetch API (with Demo)');
i.setAttribute('alt', 'GoodBye XMLHttpRequest');

//attach to the DOM
document.body.insertBefore(i, document.body.firstElementChild);
```

Or Create the element right in DOM like as follows:

```html
<img-figure 
  style="max-width: 400px"
  src="//res.cloudinary.com/time2hack/image/upload/ways-to-host-single-page-application-spa-static-site-for-free.png"
  alt="Free Static Hosting"
  caption="Ways to host single page application (SPA) and Static Site for FREE">
</img-figure>
```

### Demo:

{% codepen https://codepen.io/pankajpatel/pen/XzQLNX default-tab=js,result %}

---

Let's take a look at the component creation in detail:

### Initial Required part

All custom elements/components extend the basic HTMLElement object and have the features of it like the attributes, styles etc.

```js
class ImgFigure extends HTMLElement {
  connectedCallback() {
    // ....
  }
}
```

And the `connectedCallback` is executed when they are attached to the DOM. So we place the initial code in this function.

### Final Required Part

Finally, we need to register the element to the DOM, so that when DOM sees that element, it will instantiate the above-mentioned Class rather than `HTMLElement`.

```js
customElements.define('img-figure', ImgFigure);
```

And that's it. These parts will register the component and available to be created through `document.createElement` API.

### Play with WebComponents (another Demo):

{% codepen https://codepen.io/pankajpatel/pen/vWVwBa default-tab=result %}

---

> But what if you wanna use the make it react to any attribute change?

For that, there are two pieces of code that should be present in the Component's class.

**One:** Need to register the observable attributes:

```js
static get observedAttributes() {
  return ['attr1', 'attr2'];
}
```

**And Second:** Need to react on the observable attributes' changes:

```js
attributeChangedCallback(attr, oldValue, newValue) {
  if(oldValue === newValue){
    return;
  }
  if (attr == 'attr1') {
    // some stuff
  }
  if (attr == 'attr2') {
    // some other stuff
  }
}
```

Let's see these two pieces of code in our old `img-frame` Component:

```js
class ImgFigure extends HTMLElement {
  connectedCallback() {
    this.src = this.getAttribute('src') || null;
    this.caption = this.getAttribute('caption') || '';
    this.alt = this.getAttribute('alt') || null;
    this.render();
  }
  static get observedAttributes() {
    return ['src'];
  }

  attributeChangedCallback(attr, oldValue, newValue) {
    if(oldValue === newValue){
      return;
    }
    if (attr === 'src') {
      this.querySelector('img').src = newValue;
    }
  }
  render() {
    this.innerHTML = template({
      src: this.src,
      alt: this.alt,
      caption: this.caption,
    });
  }
}
```

{% codepen https://codepen.io/pankajpatel/pen/xPMNZw default-tab=result %}

This way you can create your custom elements without needing to worry about much of the browser support.

The life-cycle methods of the `customElement` are:

|Method|Usage/Description|
|--- |--- |
|constructor()|Called when the element is created or upgraded|
|connectedCallback()|Called when the element is inserted into a document, including into a shadow tree|
|disconnectedCallback()|Called when the element is removed from a document|
|attributeChangedCallback(attrName, oldVal, newVal, namespace)|Called when an attribute is changed, appended, removed, or replaced on the element (Only called for observed attributes)|
|adoptedCallback(oldDocument, newDocument)|Called when the element is adopted into a new document|


### Support?

[Can I Use custom-elementsv1?](https://caniuse.com/#feat=custom-elementsv1) Data on support for the custom-elementsv1 feature across the major browsers from caniuse.com.

But wait! Firefox is right there to support `customElements`:

> Summary:  
> This is basically an after the fact notification that we're in progress of implementing Custom Elements (both autonomous custom elements and customized built-in elements) and the implementation for old spec, which was never exposed to the web, will be removed. We are close to finishing the implementation, but there are still some performance works before shipping the feature. We plan to enable it _only_ on Nightly first to get more feedback from users. (there will be an intent-to-ship before shipping the feature)  
> [https://groups.google.com/forum/#!msg/mozilla.dev.platform/BI3I0U7TDw0/6-W39tXpBAAJ](https://groups.google.com/forum/#!msg/mozilla.dev.platform/BI3I0U7TDw0/6-W39tXpBAAJ)

---

Detailed Reading on CustomElements: [https://developers.google.com/web/fundamentals/web-components/customelements](https://developers.google.com/web/fundamentals/web-components/customelements)

> But What about ShadowDOM?

## ShadowDOM

ShadowDOM is a way to encapsulate the underlying DOM and CSS in a Web Component. So if you really need the encapsulation; cases when you are providing widgets to the third party; use ShadowDOM.

Primarily you can attach ShadowDOM with `attachShadow` and then perform operations on it:

```js
element.attachShadow({mode: 'open'});
```

Let's see an example of ShadowDOM:

{% codepen https://codepen.io/pankajpatel/pen/rYRLZG default-tab=js,result %}

The `attachShadow` method needs a configuration object which says only about the encapsulation. The object will have key `mode` which will have value either `open` or `closed`.

And as explained at [https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow):

`mode`: A string specifying the encapsulation mode for the shadow DOM tree. One of:

```js
element.shadowRoot === shadowroot; // returns true
```

`closed`: Specifies closed encapsulation mode. This mode denies any access to node(s) of a closed shadow root from an outside world

```js
element.shadowRoot === shadowroot; // returns false
element.shadowRoot === null; // returns true
```

The `attachShadow` returns the `ShadowRoot` which you can use as a regular document and perform operations on it.

### Support?

[Can I Use shadowdomv1?](https://caniuse.com/#feat=shadowdomv1) Data on support for the shadowdomv1 feature across the major browsers from caniuse.com.

More/Detailed reading on ShadowDOM: [https://developers.google.com/web/fundamentals/web-components/shadowdom](https://developers.google.com/web/fundamentals/web-components/shadowdom)

----

## HTML Template

The HTML templates provide the mechanism to send the markup on the page but not being rendered. This is a huge help if you wanna keep your JavaScript bundle size to minimal.

Once the template is on the document, it can be cloned and then filled with the relevant dynamic content with JavaScript

Its support is still not wide enough; so you can check that with following code

```js
if ('content' in document.createElement('template')) {
  // operate on the template
}
```

Considering that the browser being used supports the template tags; you can use them in the following way:

```html
<template id="img-figure">
  <figure>
    <img />
    <figcaption></figcaption>
  </figure>
</template>
```

```js
let template = () => `Template tag not supported`;
const t = document.querySelector('#img-figure');
if ('content' in document.createElement('template')) {
  template = (state) => {
    const img = t.content.querySelector('img');
    const caption = t.content.querySelector('figcaption');
    img.setAttribute('src', state.src);
    img.setAttribute('alt', state.alt || state.caption);
    caption.innerHTML = state.caption;
    return document.importNode(t.content, true);
  }
} else {
  template = (state) => { //fallback case
    const d = document.createElement('div');
    d.innerHTML = t.innerHTML;
    const img = d.querySelector('img');
    const caption = d.querySelector('figcaption');
    img.setAttribute('src', state.src);
    img.setAttribute('alt', state.alt || state.caption);
    caption.innerHTML = state.caption;
    return d.firstElementChild;
  }
}

class ImgFigure extends HTMLElement {
  connectedCallback() {
    this.src = this.getAttribute("src") || null;
    this.caption = this.getAttribute("caption") || "";
    this.alt = this.getAttribute("alt") || null;
    this.render();
  }

  render() {
    this.appendChild(template({
      src: this.src,
      alt: this.alt,
      caption: this.caption,
    }));
  }
}

customElements.define('img-figure', ImgFigure);
```

{% codepen https://codepen.io/pankajpatel/pen/MOdYmo default-tab=js,result %}

Read more on HTML Template here: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template)

---

## ~~HTML Imports~~ (Deprecated)

~~The HTML Imports are the easiest way to deliver the WebComponents to the desired location.~~

~~These work in the same way as you import external stylesheets in your document.~~

```html
<link rel="import" href="img-figure.html" />
```

~~And then your component file `img-figure.html` can have other dependency added, like as follows:~~

```html
<link rel="stylesheet" href="bootstrap.css">
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
...
```

~~[https://www.html5rocks.com/en/tutorials/webcomponents/imports/](https://www.html5rocks.com/en/tutorials/webcomponents/imports/)~~

---

## Help

Following places will be able to help you more in understanding the concepts of WebComponents:

*   [https://developers.google.com/web/fundamentals/web-components/](https://developers.google.com/web/fundamentals/web-components/)
*   [https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements)
*   [https://developer.mozilla.org/en-US/docs/Web/Web_Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components)

---

## People/Companies using `WebComponents`

To motivate you about WebComponents:

{% twitter 857628546507968512 %}


{% twitter 464102546114506752 %}

Others who are not very social 😉

[https://github.com/Polymer/polymer/wiki/Who's-using-Polymer?](https://github.com/Polymer/polymer/wiki/Who's-using-Polymer%3F)

---

## Final Thoughts

WebComponents are great. And then slowly all browsers are moving towards complete support.

You can use them with regular JavaScript script include as well if you are not sure about the support for HTML imports and template tags.

---

## Special Thanks

Thanks a lot **Alex** and **Nico** for helping and reviewing this post:

<center>[@nogizhopaboroda](https://github.com/nogizhopaboroda) | [@nvignola](https://github.com/nvignola)</center>

---

Let us know what you think about the WebComponents via comments.

If you are stuck somewhere while implementing WebComponents, reach out through comments below and we will try to help.

---