In many situations, you will find it useful to build a reusable javascript component.

For example, you may want to share code between projects or build an open-source UI widget. In the past, I encountered these two situations. I was working in a team that maintained dozens of applications. They all needed an interactive field to search information in an internal database. Instead of building the same feature on all the apps, we decided to build a reusable HTML/JS component. When I look back at this plugin now, I realize that we made a few mistakes in the conception.

In the first part, I will highlight and explain the modern techniques used to build reusable UI components. Then, we will create a small widget together in a second part.

Part 2 is available here

What not to do when creating a reusable web component

Using a web framework

Because we made all the apps with Angular JS, we thought it would be a good idea to use Angular to build our UI component. It wasn't.

  • It adds a peer dependency

You can't ensure all the apps will always use the same AngularJS version. What if you need to upgrade one app from Angular 1.6 to Angular 8? You won't be able to use the widget anymore, which brings us to the next point

2. You will likely need to bundle the web framework in your widget

This will increase the size of your bundle and slow down your application.

Using generic CSS class names

Your CSS code must not clash with the one the app defines. This could break your widget, or worse, the app that integrates your widget. There are a few options available to sanitize your class names. You could use a naming convention, like the BEM method developed by Yandex. Or, you could host the widget on your servers and use an iframe. Although both solutions would work, the W3C has introduced a new system to address this problem.

Introducing Web Components

In July 2014, the W3C drafted the web components. It is composed of 4 functionalities:

  • the shadow DOM ;
  • the custom elements ;
  • the HTML imports ;
  • the HTML templates.

We will now go through each of them to see how they can help us build our widget.

The shadow DOM

In an HTML document, all elements and styles belong to a single global scope. This can be problematic if you are building a component or widget. There is no guarantee that when used in a page, it will not conflict with other elements or CSS classes.

The shadow DOM lets you add an isolated DOM inside the document. Any style or javascript appended to the shadow root will not be able to modify anything outside of its scope. Likewise, the styles and scripts from the page will not touch your widget.

License: CC BY 4.0

Implementing a shadow DOM is straight forward. All you have to do is calling  the function attachShadow() on the element you want to turn into a shadow DOM.

In the following example, I attached a shadow DOM to the shadow-host <div> and appended a paragraph to it. Even though the CSS specifies that all paragraphs should be red, the paragraph inside the shadow root stays black.

The custom elements

The basic idea is that if you create an element that always plays the same role, you should be able to give it a name that describes what it does. We already have the video element to integrate a video and the article element to display an article. Many elements already describe their function. Why shouldn't we be able to create our own elements?

We will now create a <colored-paragraph> element, that will display a colored paragraph. Defining your own custom element is really simple. All you have to do is calling customElements.define to register it.

The function takes 3 arguments:

  1. the name of your element, ie: colored-paragraph (note: the dash avoids name collisions with any native HTML elements and is mandatory)
  2. an ES6 class that inherits HTTPElement
  3. an option object which can indicate the built-in element your element inherits, if any.

Attributes

You can pass a list of attributes to your element. In our colored-paragraph, we will pass a color attribute that lets you select the color to display.

When appending the element to the DOM, you will just have to specify the color you want to use.

<colored-paragraph color="red">Hello, world!</colored-paragraph>

In the class, you can get the value given to an attribute with:

this.getAttribute('color');

The following example contains a simple implementation of our colored-paragraph.

Have you noticed how I used the shadow DOM? Attaching a shadow DOM to the custom element ensures the external CSS won't be able to modify it.

Lifecycle

As our class inherits from HTMLElement, there are a few lifecycle hooks you can implement.

The first one is the constructor. This is where you will add all the empty <div>s and <style>s.  It is a good practice to use a shadow DOM to completely isolate your element.

constructor () {
  super() 
  
  const shadowDOM = this.attachShadow({ mode: 'open' })     
       
  const style = document.createElement('style');
  const text = document.createElement('p')
  text.innerHTML = this.innerHTML
     
  shadowDOM.appendChild(style)
  shadowDOM.appendChild(text)    
}

Then, we have connectedCallback and attributeChangedCallback. The first is called when the element is mounted in the DOM. The second is called when one of the attributes changes. I usually create a function that updates the style and call it in  each of these 2 hooks. You also need to specify the attributes to observe if you want to dynamically update your element.

static get observedAttributes() {
  return ['color'];
}

updateStyle = (elem) => {
  const shadow = elem.shadowRoot;
  const color = this.getAttribute('color');

  shadow.querySelector('style').innerHTML = `p { color: ${color} }`;
}

connectedCallback() {
  this.updateStyle(this);
}

attributeChangedCallback(attr, oldVal, newVal) {
  this.updateStyle(this);
}

In the following example, I implemented a button that dynamically changes the color of my element. To do this, I grabbed a reference to the element using querySelector and called the setAttribute function on it. As color is an observed attribute,  this will trigger attributeChangedCallback that will update the color.

In the next article, we will define our widget as a custom element.

HTML templates

With HTML templates, you can declare a fragment that can be cloned and inserted in the DOM. Your template will not render until you activate it using Javascript. The templates will be useful to us as the widget grows. We can for example use them to setup a custom element initial structure.

Let's say you're building a widget that displays a list of articles, supposedly to display your latest blog posts on your landing page. Defining the following template would help you initiate the structure of each article.

<template id="article-template">
  <li class="article">
    <div class="article-author"></div>
    <div class="article-title"></div>
    <div class="article-body"></div>
  </li>
</template>

To render the template, you can use the following javascript snippet:

let template = document.getElementById('article-template');
document.body.appendChild(template.content.cloneNode(true));

HTML Imports

HTML imports are supposed to allow you to import HTML documents into other HTML documents. I said "supposed" because they have poor browser support. Their specification is incomplete and that is why many browsers, including Mozilla Firefox, have decided not to implement them.

We won't use them in the second part of this tutorial because it does not make sense to only target 40% of all user. However, I wanted to mention them as they will become really handy in the future when it comes to writing reusable components. Using them with custom elements, html templates and the shadow DOM will let you write importable widgets that can be used anywhere in the document.

So, if I wanted to import the colored paragraph custom element we built earlier in the page, all I would have to is:

<link rel="import" href="/colored-paragraph.html">

<colored-paragraph color='red'>
</colored-paragraph>

I hope you enjoyed reading the first part of this tutorial. In the next part, we will create a reusable component together to demonstrate all the things we've learnt. If you have any question, don't hesitate to ask them in the comment section.

Part 2 is available here