
Modularize websites with Web Components
Web Perfect
Microservices are being used more and more as architecture patterns for client-server applications. Developers break down monolithic back ends into smaller services that can be developed independently of each other and, if necessary, re-implemented with little effort.
Web developers naturally ask themselves how they can best integrate the multitude of small services into their websites. One answer is with Web Components [1], self-adjustable and reusable HTML tags that are standardized, work across browsers, and can be used with all JavaScript libraries and frameworks that also work with HTML (see Table 1).
Tabelle 1: Native Use of Web Components
Browser |
Version |
Example Works? |
---|---|---|
Chrome |
69 |
Yes |
Firefox |
63 |
Yes |
Safari |
12 |
Yes (untested) |
MS Edge |
42 |
No |
For example, developers can use Web Components to integrate microservices into web pages without diluting HTML documents with redundant ID
attribute values or additional stylesheet information.
Fully Integrated
Web developers integrate individual components with HTML tags they define themselves. In Lines 8-10 of Listing 1, this tag is <anot-list> ... </anot-list>
. Lines 4 and 5 include the appropriate script components.
Listing 1: <anot-list> and <anot-title>
01 [...] 02 <head> 03 [...] 04 <script src="https://unpkg.com/@webcomponents/webcomponentsjs"></script> 05 <script type="module" src="anot-list.js"></script> 06 </head> 07 <body> 08 <anot-list> 09 <anot-title>My Comments</anot-title> 10 </anot-list> 11 <article><Dummytext></article> 12 <todo-list> [...] </todo-list> 13 [...]
The JavaScript code (Listings 2-5) resolves the tags into a document fragment that includes stylesheet information. (For readability, the code is divided into several listings.) Lines 26-28 of Listing 5 define the self-designed HTML tags at the end. However, the adapted document fragment ends in a shadow Document Object Model (DOM) after it has been created, where <anot-list>
assumes a role as a root element (Figure 1).

Shadow DOM
The new DOM feature exists outside the actual document tree. The code creates a shadow root to mount document fragments under a specific host element, which creates a subtree for the element, known as a shadow DOM. The adapted new element can then be constructed in it.
Using the associated shadow DOM API, developers can address and configure embedded elements more elegantly than by using the unpopular and bulky <iframe>
tag. The concrete result is an interactive comment function (Figure 2) that appears to the right of the dummy text. Figure 3 shows the associated source code for the new item in the browser's developer console.

<anot-list>
Web Component generates a comment function that saves locally (shown in the Firefox browser view on Ubuntu).
Web Components can be used universally because of their weak interaction with other document components. Google offers such elements with the open source Polymer [2] framework; Mozilla's library of Web Components is known as X-Tag [3]. The Mozilla Developer Network offers more information [4] on this topic, as well. Additionally, the recently released Firefox 63 browser version supports a "modern Web Components API" [5].
In the Light of the DOM
Web Components themselves are not standardized but are described in the style of a design pattern [6]. They are generated in the interplay between custom elements [7], the shadow DOM [8], and the HTML templates [9].
The HTML templates are used to generate user-defined tags, as shown in Listing 5 (lines 26-28). Thanks to the shadow DOM, redundant ID attribute values and stylesheet details can be avoided. HTML templates also make it possible to write document fragments as HTML tags, as demonstrated in Listing 3 (lines 1-12).
Modular
For some time now, the JavaScript [10] module system from the HTML specification has replaced HTML imports when loading the Web Components. To upgrade missing features in the browser, line 4 of the HTML document in Listing 1 loads the webcomponentsjs polyfills via a <script>
tag.
This URL offers all modules from Node's official package archive [11]. You can refine the selection by specifying certain operators. The web page loads the package if the name is followed by @1.0.16
.
The code that resolves the <anot-list>
and <anot-title>
user-defined tags is loaded in line 5 of Listing 1. In line 11, the code saves the sample text in the HTML5 <article>
tag.
Shadowy
The JavaScript code generates the Web Components by evaluating the function call in the (function(){
…})
expression (Listing 2, lines 1-6). The anonymous function block prevents the constants, variables, and classes declared in the listings from falsifying the global namespace of the JavaScript environment by overwriting it.
Listing 2: anot-list.js
01 (function() { 02 class AnotElement extends HTMLElement { 03 constructor() { 04 super(); 05 this.attachShadow({'mode': 'open'}); 06 } 07 08 listItem() { 09 const arr = JSON.parse(window.localStorage.getItem('annotations')); 10 return arr?arr:[]; 11 } 12 13 hasItem(text) { 14 return text == '' || this.listItem().filter(stext => text === stext).length > 0; 15 } 16 17 saveItem(text) { 18 if (!this.hasItem(text)) { 19 const arr = this.listItem(); 20 arr.push(text); 21 window.localStorage.setItem('annotations', JSON.stringify(arr)); 22 } 23 } 24 25 delItem(text) { 26 const arr = this.listItem().filter(stext => text !== stext); 27 localStorage.setItem('annotations', JSON.stringify(arr.length>0?arr:[])); 28 } 29 }
The <anot-list>
, <anot-title>
, and <anot-item>
tags defined by the developer are first declared as classes in Listings 3-5, respectively. The parent class AnotElement
(Listing 2) is derived from the built-in HTMLElement
class.
The constructor()
function of AnotElement
instantiates a shadow root as the root element for the shadow DOM associated with the host element (Listing 2, line 5). It calls the attachShadow()
method and stores its reference implicitly in the shadowRoot
property.
Stored Locally
The methods in Listing 2 give each class instance an API to store the application data. The interface uses the localStorage
browser object to store the data under the local browser instance in the form of a serialized field for the annotations
key (line 9). For serializing, Listing 2 uses the stringify()
method to deserialize the parse()
method of the global JSON object (line 9). At this point, the listItem()
method reads the serialized field with getItem()
from localStorage
and converts it to an array. Line 10 ensures that a field is returned.
The hasItem()
method tests the stored field for duplicates in lines 13-15. The filter()
array method searches the list for hits, and saveItem()
saves a new comment from the call parameter text
(line 17). To do this, line 20 appends the value of text
to the list of existing comments, and line 21 serializes the extended list and writes it back to localStorage
by calling the setItem()
method.
An almost identical expression is used in line 27, which saves the list cleaned up by calling the delItem()
method (lines 25-28) in localStorage
again. The filter()
in line 26 sifts through the text
object that is passed in.
Template Added
Line 1 of Listing 3 creates an initially empty HTML element of type <template>
and stores its reference in the local constant anotList
. Lines 2-12 initialize the template in declarative style, which means they specify an HTML code fragment in the form of a string. The script assigns the innerHTML
property to the string and copies it into the template.
Listing 3: anot-list.js (continued)
01 const anotList = document.createElement('template'); 02 anotList.innerHTML = ` 03 <style> 04 ul { 05 list-style: none; 06 padding:0; 07 } 08 </style> 09 <slot></slot> 10 <ul id='list'></ul> 11 <input id="text" type="text"> 12 <button id="add" type="button">+</button>`; 13 14 class AnotList extends AnotElement { 15 constructor() { 16 super(); 17 this.shadowRoot.appendChild(anotList.content.cloneNode(true)); 18 this.shadowRoot.querySelector('#add').addEventListener('click', e => this.addItem()); 19 this.listItem().forEach(text => this.showItem(text)); 20 } 21 22 addItem() { 23 let text = this.shadowRoot.querySelector('#text').value; 24 if (!this.hasItem(text)) { 25 this.saveItem(text); 26 this.showItem(text); 27 } 28 } 29 30 showItem(text) { 31 var listed = new AnotItem(text); 32 this.shadowRoot.querySelector('#list').appendChild(listed); 33 this.shadowRoot.querySelector('#text').value = ''; 34 this.shadowRoot.querySelector('#text').focus(); 35 } 36 }
Lists and Buttons
The entries in the <style>
tag format the HTML list in the <ul>
element in line 10. The list contains the saved comments. The <input>
element in line 11 enters new comments, and the <button>
element in the following line confirms the user input.
Line 17 of the constructor()
function for the class AnotList
, which starts in line 15, creates a copy of the template by cloning (cloneNode()
), which the script moves to the end of the shadow root by calling the appendChild()
method.
If the user has clicked on the aforementioned button, line 18 initiates the addItem()
callback function. Line 23 copies the value from the <input>
field and saves it in the text
variable.
If the call to the hasItem()
method together with the given value in line 24 of Listing 3 fails, line 25 saves the new comment text under localStorage
. Line 26 then visualizes it in the HTML document by calling the showItem()
method, which occurs in line 19 for each stored comment thanks to the calling constructor()
function.
Next, look at the showItem()
method (lines 30-35), which instantiates an object of the AnotItem
class in line 31. The listed
constant temporarily stores the returned reference to the list entry. The next line appends the cached element to the end of the HTML list (<ul id="list"/>
in line 10). Line 33 then empties the input field, and line 34 gives it the focus for further comments.
Slots
The <slot>
tag in line 9 now makes a late appearance. It takes on a special task and serves as an insertion mark for elements from outside in the shadow DOM standard, which plays that role in this example in line 9 of Listing 1 (i.e., <anot-title>My Comments</anot-title>
.
The AnotTitle
class resolves the user-defined element <anot-title>
in line 11 of Listing 4. The design pattern for <anot-list>
is used again when creating the Web Components. In line 9, the template uses <h3>
and another <slot>
tag, which causes the My Comments
text node from line 8 of Listing 1 to appear as a headline for the <anot-item>
added to the shadow DOM externally.
Listing 4: anot-list.js (continued)
01 const anotTitle = document.createElement ('template'); 02 anotTitle.innerHTML = ` 03 <style> 04 h3 { 05 margin: 0; 06 text-align:center; 07 } 08 </style> 09 <h3><slot></slot></h3>`; 10 11 class AnotTitle extends AnotElement { 12 constructor() { 13 super(); 14 this.shadowRoot.appendChild(anotTitle.content.cloneNode(true)); 15 } 16 }
Delete Function
Listing 5 declares <anot-item>
. After line 15 has cloned the corresponding template and completed it in its own shadow DOM, the next line copies the comment text from the text
call parameter to the <li>
element (line 10). This is again done by assigning the innerHTML
property.
Listing 5: anot-list.js (continued)
01 const anotItem = document.createElement('template'); 02 anotItem.innerHTML = ` 03 <style> 04 li { 05 padding:0.3rem; 06 margin:0.2rem; 07 background-color: var(--anot-item-bg, #f3f315); 08 } 09 </style> 10 <li id="text"></li>`; 11 12 class AnotItem extends AnotElement { 13 constructor(text) { 14 super(); 15 this.shadowRoot.appendChild(anotItem.content.cloneNode(true)); 16 this.shadowRoot.querySelector('#text').innerHTML = text; 17 this.shadowRoot.querySelector('li').addEventListener('click', e => this.rmItem(text)); 18 } 19 20 rmItem(text) { 21 this.remove(); 22 this.delItem(text); 23 } 24 } 25 26 customElements.define('anot-title', AnotTitle); 27 customElements.define('anot-item', AnotItem); 28 customElements.define('anot-list', AnotList); 29 })();
Thanks to line 17, a click on the <li>
tag calls the delete routine rmItem()
. The routine removes the list item from the display with remove()
; then, delItem()
deletes the comment from localStorage
.
Worth Considering
Because the JavaScript code shown follows the ECMAScript 6 standard, the reference variable this
in the lambda functions of lines 18 (Listing 3) and 17 (Listing 5) refers to the current object and not, as in the previous JavaScript specifications, to the event-triggering element (i.e., the button or the list entry).
Developers should never forget the call to super()
in constructor()
functions of derived classes (e.g., line 14 of Listing 5). At the end of the listing, the calls in lines 26-28 register the presented classes under the corresponding tag as custom elements.
Into the Microservice
To immerse yourself completely in the waters of microservices, you will want to store the comments by way of a back end in a central database on the server. In the best case, you would use XHTTP requests.
To identify the commentator, program a second microservice that exchanges data with the first over a suitable protocol. Simply encapsulate the login dialog on the website in another <user-auth>
Web Component, which deposits the user ID in the browser's sessionStorage
userid
key after a successful login.
Breakthroughs
Listing 6 shows how to format Web Components with stylesheet information from outside the shadow DOM. Elements in the shadow DOM take formatting data, as usual, from the browser's standard stylesheet or from the <Body>
element (e.g., font-family
in line 2). The shadow root can be formatted with the tag name (lines 8-16). However, rather than inheriting any format specifications, it only configures the rectangular area in which the browser renders the Web Components.
Listing 6: CSS for Web Components
01 body { 02 font-family: sans-serif; 03 width:80%; 04 max-width:960px; 05 margin:auto; 06 } 07 08 anot-list { 09 position:fixed; 10 right: 7%; 11 width: 18%; 12 margin-top: 2em; 13 padding: 2em; 14 border-radius: 0.5em; 15 border:1px solid #aaa; 16 } 17 18 article { 19 margin: auto; 20 float:left; 21 margin-left: 20%; 22 margin-right: 30%; 23 } 24 25 li { 26 --anot-item-bg: #ff6699; 27 }
The inner workings of the shadow DOM can otherwise only be formatted by CSS hooks. The specification
background-color: var(--anot-item-bg, #f3f315);
from line 7 of Listing 5 declares a hook for the background color of the list elements. It sets the six-digit RGB value behind the hash mark to neon yellow by default. Line 26 of Listing 6 finally overwrites the color value with neon red, as an example.
Conclusions
Web Components offer the web developer an important design pattern for integrating small microservices into their websites. Custom elements enable you to design universal declarative identifiers for the components. The shadow DOM reduces conflicts with the rest of the document and provides interfaces for programming and formatting. Use of the design pattern is cultivated by Google's Polymer open source framework, which provides an entire zoo of ready-made Web Components free of charge.