DEV Community

Cover image for Introduction to WebComponents with ShadowDOM
Pankaj Patel for Time to Hack

Posted on • Originally published at time2hack.com

Introduction to WebComponents with ShadowDOM

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:

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 logo webcomponents / polyfills

Web Components Polyfills

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:

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

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

And the component will look like:

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

The basic component code will be as follows:

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);
Enter fullscreen mode Exit fullscreen mode

And its usage through JavaScript will be as follows:

// 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);
Enter fullscreen mode Exit fullscreen mode

Or Create the element right in DOM like as follows:

<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>
Enter fullscreen mode Exit fullscreen mode

Demo:


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.

class ImgFigure extends HTMLElement {
  connectedCallback() {
    // ....
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

customElements.define('img-figure', ImgFigure);
Enter fullscreen mode Exit fullscreen mode

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

Play with WebComponents (another Demo):


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:

static get observedAttributes() {
  return ['attr1', 'attr2'];
}
Enter fullscreen mode Exit fullscreen mode

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

attributeChangedCallback(attr, oldValue, newValue) {
  if(oldValue === newValue){
    return;
  }
  if (attr == 'attr1') {
    // some stuff
  }
  if (attr == 'attr2') {
    // some other stuff
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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? 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


Detailed Reading on 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:

element.attachShadow({mode: 'open'});
Enter fullscreen mode Exit fullscreen mode

Let's see an example of ShadowDOM:

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:

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

element.shadowRoot === shadowroot; // returns true
Enter fullscreen mode Exit fullscreen mode

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

element.shadowRoot === shadowroot; // returns false
element.shadowRoot === null; // returns true
Enter fullscreen mode Exit fullscreen mode

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

Support?

Can I Use 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


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

if ('content' in document.createElement('template')) {
  // operate on the template
}
Enter fullscreen mode Exit fullscreen mode

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

<template id="img-figure">
  <figure>
    <img />
    <figcaption></figcaption>
  </figure>
</template>
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode

Read more on HTML Template here: 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.

<link rel="import" href="img-figure.html" />
Enter fullscreen mode Exit fullscreen mode

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

<link rel="stylesheet" href="bootstrap.css">
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
...
Enter fullscreen mode Exit fullscreen mode

https://www.html5rocks.com/en/tutorials/webcomponents/imports/


Help

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


People/Companies using WebComponents

To motivate you about WebComponents:

Others who are not very social 😉

https://github.com/Polymer/polymer/wiki/Who's-using-Polymer?


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:

@nogizhopaboroda | @nvignola

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.


Top comments (3)

Collapse
 
seanmclem profile image
Seanmclem • Edited

I've never seen HTML Imports lumped in with specs that make up web components. It seems like both Chrome and Firefox are not implementing it so far.

developer.mozilla.org/en-US/docs/W...

Collapse
 
faahdnoor profile image
Fahad Noor

Html imports are deprecated.

Collapse
 
pankajpatel profile image
Pankaj Patel

Agree.