Building web blocks

When you approach web components the sheer volume of material can be overwhelming. Behind the one term there are two specifications and three core technologies, coming together to forge any widget a web developer may need. To ease in the topic, however, it is possible to focus on the three key ingredients one at a time.

Indeed, while the technologies are often bundled together in more complex components, they are also independent of one another, and each has its own reason for being.

Custom elements

Explained in the HTML Living Standard, custom elements are elements you create yourself, with tags and a name made of multiple, hyphenated words.

<hopeful-message></hopeful-message>

You need more than one word to separate the elements from standard DOM nodes.

On its own the code achieves little but creating a generic container, and it is with JavaScript that you build up the feature, starting with a class extending an HTMLElement.

class HopefulMessage extends HTMLElement {
	constructor() {
		super();
	}
}

It is convenient to repeat the label used for the element in camel case, if only to save the hassle of finding another name.

In the constructor method, invoking super makes it possible to extend the features built in the element itself, validating the node. In the same scope, you can update the element any way you may choose. You can, for instance, inject markup through the innerHTML property, on the very same element available through the this keyword.

super();
this.innerHTML = '<p>I see the path</p>';

The class exists, but you need to create a connection with the specific element. And for this, the customElement interface offers the define function.

customElements.define('hopeful-message', HopefulMessage);

In the first argument point to the element, by name, and in the second refer to the class.

Have the script run, and you are sure to find the paragraph on the page. Inspect the document, and sure enough, you’ll be happy to find evidence of the element’s custom nature.

Inspector <hopeful-message> <p>...</p> </hopeful-message>

Shadow DOM

Moving into the DOM Living Standard, the shadow DOM describes a way to encapsulate code. The term shadow is set to oppose the regular DOM, which you may dub as light DOM, and you can witness its influence with another small script. The effort is rather self-serving, but works to illustrate the point.

Add a div container on the page.

<div></div>

With JavaScript, you can target the node and “enter” the shadow DOM by attaching a root with the attachShadow function.

const div = document.querySelector('div');
const shadowRoot = div.attachShadow({ mode: 'open' });

The function itself requires an argument, an object with a property of mode, but without going into details about the two available options, "open" and "closed", focus on the returned value, on the shadow root.

You can update this root with markup and the same property available on HTML elements, innerHTML.

shadowRoot.innerHTML = `<p>From this dark place</p>`;

The text will be displayed still, but inspect the page, and you’ll see that the content is nested an additional level, in the promised shadow DOM.

Inspector <div> #shadow-root (open) <p>...</p> </div>

Here the markup is encapsulated. Should you style the elements in the root, even with more general CSS selectors.

shadowRoot.innerHTML = `<style>
    p {
        color: burlywood;
    }
</style>
<p>I see my future</p>`;

You’ll see that the code affects only the nodes in the same scope, and not those in the rest of the page. In truth, some properties are inherited, and there are ways to further customize the elements from outside, but this is in essence the shadow DOM, a way to create a clean cut.

HTML templates

Back in the HTML specification, the template element lets you author some markup for later use.

<template>
	<!-- ... -->
</template>

Consider a card, highlighting holidays in title and date.

<article>
	<h1></h1>
	<time></time>
</article>

Add the code between the opening and closing template tags and you’ve built your first template. On screen you won’t find a trace of the article, but inspecting the source you will find the template element and a nested #document-fragment.

Inspector <template> #document-fragment <article>...</article> </template>

The content will be rendered, but only as required, only with JavaScript. And while the process is slightly involved, it quickly solves the templating function.

In a script isolate the template and create a clone of the element’s content. You do this with the cloneNode function.

const template = document.querySelector('template');
const content = template.content.cloneNode(true);

The step is essential for a simple reason, but to complete the task at hand, focus on the cloned content. The input argument, true, makes it possible to create a deep copy, so that you have a reference to all the elements housed in the template. And through the reference, you have the option to update the nodes and celebrate an upcoming festivity.

const heading = content.querySelector('h1');
const time = content.querySelector('time');

heading.textContent = 'Easter';
time.textContent = '31s March';
time.setAttribute('datetime', '2024-03-31');

Author the markup and append the content in a visible place.

document.body.appendChild(content);

On the page you’ll finally see the result. And should you inspect the DOM, you’ll still see the template and the nested content, waiting to be used once more.

This last fact helps to justify the rather cryptic line cloning the content. What happens if instead of resorting to the cloneNode function you refer to the content directly?

-const content = template.content.cloneNode(true);
+const content = template.content

In the specific example the result is the same. The holiday is displayed in name and instant. The template, however, can no longer be used. If you analyze the DOM tree, one last time, you can attest yourself that the element is indeed spoiled of the markup.

Inspector <template> #document-fragment </template>

In short, if you want to use and reuse a template, remember to clone the content for good. And, if for some strange reason you are still opposed to cloneNode, you’ll be glad to discover that there is an alternative through the document object, and the importNode function.

-const content = template.content.cloneNode(true);
+const content = document.importNode(template.content, true);

The two achieve the same exact goal, to have a copy of the content and preserve the original. A template, after all, is a guide, meant to be used again and again.

Of note, and similarly to the code introduced for the shadow DOM, you can have a <style> tag in a template element as well.

<template>
	<style>
		p {
			color: burlywood;
		}
	</style>
	<!-- ... -->
</template>

This time, however, the key-value pairs affect other elements, bringing the topic full circle. There are three technologies separate from each other, but working together to build full-fledged components. Components which follow a convenient template, are safely styled through the inscrutable shadow DOM and come with their own tag.

<custom-counter></custom-counter>

Just waiting to be of use.