The UI is somewhat different from other layers. First, It took time to realize that UI needs to be separated into units. Back in the vanilla or JQuery days the html was flat on the page and we applied behaviour using javascript which was the best thing at the time. It has kind of reusability, but the unit did not expose any high-level API. Any major change in the unit needs to be supported by the consumers.

Nowadays we use frameworks or libraries like Angular, React, Vue, Elm or whatever is out there. We do create components with high-level API and these components can be extracted from a project or created in a module so any web app can reuse. Well, actually not any web app. Only those who are using the same framework as the component. If you want to expose the components to all frameworks you need to extract whatever you can into a native core layer, then wrap in again with a specific framework implementation. Every change in the core needs to be reflected in all of the implementations.

Photo by Nathan Dumlao on Unsplash

Web Components to the rescue. “Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps” — MDN. With web component we can create reusable UI with high-level API regardless of the frameworks using it. Frameworks works with web components as they are with regular native html elements like inputs and buttons.

The Web Components standard consist of 4 parts: HTML templates which allows to reuse smaller HTML parts, HTML imports which allows to separate html parts into different units , Shadow DOM which allows to isolate the component style and behaviour and Custom Element that wraps everything together and expose high-level API.

All unknown elements are shown in the DOM tree as of type HTMLUnknownElement with no scripts, no styles and no structure. To create a web component we are going to need to register a custom element with a tag name and implemented a class that will extend HTMLElement and will add the scripts, styles and a structure.

To register a new Custom Element, we are using the customElements API:

customElements.define(‘word-count’, WordCount);

When using word-count, a new instance of WordCount will be instantiated and attached to the element. WordCount will count the words in a text attribute given to it and display a custom message. To display a custom message we will need to create a shadow DOM that will hold the message, then building the message itself and add it to the shadow DOM.

To create a shadow DOM, inside the custom element class contractor:

const shadow = this.attachShadow({ mode: ‘open’ });```

To create the initial HTML structure, we created a separate file which will be loaded using HTML import. In that file we declared an HTML template to instantiate in the component. HTML templates are not accessible to DOM queries and scripts in the template will not run until we load the template to preserve the encapsulation we strive for. In the template created a style tag that affects only elements inside the shadow DOM. Slot elements serves as placeholders for the light DOM.

In the implementation we have style definitions, title section with two slots — one named and the other don’t, and custom elements. The values in the slot tags are default values — If the consumer won’t specify any value this is what will be shown.

To load the HTML file with HTML import we are going to add a new link tag as follows:

<link rel=”import” href=”word-count/word-count.html” id=”word-count-link”>

The rel=”import” tells the browser that we are going to load the file using HTML import. The browser will download the file in a non blocking way and will make sure that it will not load more than once. To load the file and use its content we can add a block of code in the bottom of the file that adds it to the document, or we can load it manually and use it in our code. We will load it manually because we want to keep the encapsulation and we don’t want to leak data into the global scope.

To load the file and instantiate the template:

const shadow = this.attachShadow({ mode: ‘open’ }); const wordCountLink = document.getElementById(‘word-count-link’) as HTMLLinkElement; const wordCountFile = wordCountLink.import as any; const templateWordCount = wordCountFile.getElementById(‘template-word-count’); shadow.appendChild(document.importNode(templateWordCount.content, true));

Here we are creating a shadow DOM, getting the link element, import the file, query the imported content for the template and instantiating the template using document.importNode which creates a deep copy of the template.

To count the number of words in a given text we need to read the text attribute and change the DOM accordingly. We also need to keep the DOM consistent with every change of the text attribute. We can use the attributeChangedCallback callback that will fire every time an attribute will change. attributeChangedCallback is one of four lifecycle event we can use for a web component. The other ones are connectedCallback, disconnectedCallback and adoptedCallback. attributeChangedCallback will be fired only for attributes that we will declare in a observedAttributes static property. To access the shadow DOM we can use this.shadowRoot.

Here the full implementation of WordCounter:

To compose everything together we can use any framework we want. All frameworks and libraries are able to use custom elements with its API just as they are doing to native elements like inputs and buttons. Here, for example, the text attribute is our high-level API and we are going to change it to affect the component.

Using React class and MobX:

The basic flow of changes is quite simple. When the input field changes, the text attribute given to our custom component changes. In our custom component, because observedAttributes returns ‘text’, attributeChangedCallback fires and call updateWordCount to get the .word and the .word-count containers and update their innerText with the correct values.

Web components are a great idea, but not everything is bright. One of the biggest problems in the front-end world takes place here also — browser support. Not all major browser supports web components. We still need to load polyfills to use them. Moreover, polyfills can not fill everything. For example the shadow DOM encapsulation can not be fixed so we still need to be careful with the custom styles, scripts and ids. The future is yet to come.

Edit: to read about how to build web components today using Polymer, read all about it in my next post.