/ WebComponents

Introduction to WebComponents

introduction-to-webcomponents-shadowdom
WebComponents are 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
  • ShadowDOM
  • HTML Template
  • HTML Imports

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.

https://github.com/webcomponents/webcomponentsjs

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 following:

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

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

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);

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);

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>

Demo:


Lets 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() {
    // ....
  }
}

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);

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 on the Component's class.

One: Need to register the observable attributes:

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

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

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,
    });
  }
}

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

The lifecycle 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 a 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 third party; use ShadowDOM.

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

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

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:

  • open: Specifies open encapsulation mode. It means elements of the shadow root are accessible from an outside world using element.shadowRoot

    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

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

It's support is still not wide enough; so you can check that with following code

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

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

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

Read more ont HTML Template here: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template


HTML Imports

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

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

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

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

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 for WebComponents:

Others who are not very social 😉

https://github.com/Polymer/polymer/wiki/Who's-using-Polymer%3F


Final Thoughts

WebComponents are great. And the 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:

Follow @nogizhopaboroda Follow @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.