Web Components with Vanilla JavaScript

If there is something true about JavaScript and the WEB is that there are many frameworks. You have Vue, React and Angular just to name a few. I had the opportunity to work with them and they are great, but none of them makes me happier than code using Vanilla JavaScript to create Web Components.

Web Components are a set of web platform APIs that allows the creation of custom, reusable and encapsulated HTML tags. Web Components are known by the combination of four main specifications: Custom Elements, Shadow DOM, HTML Imports and HTML Template Element. It's false to say that all four are required to describe a Web Component though. For instance, you could have a Web Component without using Shadow DOM.

Being Web Components based on existing web standards, what appeals to me is the ability to extend HTML without adding any extra libraries or frameworks, although it's totally possible. In a way, it brings a sense of future proof to our applications and components and souls. Let's see an example of how to use it.

A super button

Of course, the name of the component is exaggerated, but it will serve the purpose of illustrating how to extend HTML with a new element with encapsulated styling and custom behavior.

Before continuing, an important aspect we must take notice is that at this moment, some browsers are still updating their standards for Web Components, which can make things not to work properly. In order to fix this, we can take advantage of polyfills. Just in case you have decided to use NPM to manage your dependencies, you can add it by running the following command:

npm install --save @webcomponents/webcomponentsjs

With the polyfill in place, now we can ensure it will work for browsers that not yet support Web Components fully. It's important to say we can feature-detect whether the browser supports or not a certain feature. Detecting it beforehand can reduce the overhead to load our application.

Now, there are different ways of organizing your files within the application. Some prefer to create separated files for styling, HTML and JavaScript. Others prefer to have everything within the same file. The way I prefer is to have an HTML file with the markup and styling and another file with the JavaScript. Both of them go inside of a folder with the same name as the element, as follows:

.
├── great-button
│   ├── great-button.html
│   └── great-button.js
├── super-button
│   ├── super-button.html
│   ├── super-button.js
├── index.html
super-button.html
<template id="super-button-template">
  <style>
    button {
      padding: 20px;
      outline: 0;
    }
  </style>
  <button type="button">0 click(s)</button>
</template>

<script src="super-button.js"></script>

As we can see in the snippet above, the template tag holds our fragments of markup for this component. During the page load, the template isn't used. Though, it will be instantiated after at runtime. At the bottom of the file we link our JavaScript file, that is responsible for instantiating the component and apply logic.

super-button.js
(function () {
  const ownerDocument = document.currentScript.ownerDocument;

  class SuperButton extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({mode: 'open'});
      const template = ownerDocument.querySelector('#super-button-template');
      this.shadowRoot.appendChild(document.importNode(template.content, true));

      this.counter = 0;
      this.button = this.shadowRoot.querySelector('button');
      this.button.style.backgroundColor = this.getAttribute('color');
    }

    connectedCallback() {
      this.button.addEventListener('click', () => {
        this.button.innerText = `${++this.counter} click(s)`;
      });
    }
  }

  customElements.define('super-button', SuperButton)
})();

As we can see in the snippet above, many important things are happening. First, an important recommendation is to wrap the component within an IIFE function. Second, we must extend HTMLElement which will provide us the base for the component. The creation of our custom element starts in the constructor and pass through the following stages:

  • When using shadow DOM, we must attach it to the current component by calling this.attachShadow. The mode open indicates that we have access to the shadow doom from JavaScript outside of the root. Closed means with don't have access outside of the root. For example, if we call this.shadowRoot with closed mode, it will return null.
  • We'll need to query our template within the owner document which is accessible through the current script being executed at the moment. We have access to it by calling document.currentScript.ownDocument.
  • We'll need to parse our declared markup within the template tag previously created, which will be appended after to our shadow DOM. We import it by calling document.importNode(template.content, true).

Now we have our super button created. As previously mentioned, when we extend the HTMLElement class, it provides the base for our element, including the ability to be called back when important events occur. For instance, we can be called back when the element is appended or removed from the document or when an attribute is added, removed or changed. As we can see in the snippet above, the function connectedCallback will be called when the element is appended which allows us to initialize what we need as events, data fetching and so on.

Last but not least, we need to define our custom element by calling customElements.define('super-button', SuperButton). Something important is that we need to have a compound name with a dash, so we can be recognized as a valid custom element. Once we have done it, we can glue everything together as follows:

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Custom Component</title>
  </head>
  <body>
    <super-button color="red"></super-button>

        <!-- Polyfill -->
    <script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>

    <!-- Custom root element -->
    <link rel="import" href="super-button/super-button.html">
  </body>
</html>

In the above snippet, two things are important to mention: first, the polyfill is imported so we can ensure it will work for browsers that don't support it fully. Second, we use the HTML Import specification to import our custom element that will be recognized as just a regular tag.

Conclusion

Well, I would say Web Components with Vanilla JavaScript is serving the product at work quite well. Though, if you feel like you're writing too much boilerplate code, you might want to check out Stencil. The important thing here is whether the tool you're using is providing you ways of delivering a good product or not. Another good thing is to be able to use these components within those frameworks I mentioned at the beginning.

See ya!