In our last article, we discussed the Web Components specifications (custom elements, shadow DOM, and HTML templates) at a high-level. In this article, and the three to follow, we will put these technologies to the test and examine them in greater detail and see how we can use them in production today. To do this, we will be building a custom modal dialog from the ground up to see how the various technologies fit together.
Article Series:
- An Introduction to Web Components
- Crafting Reusable HTML Templates (This post)
- Creating a Custom Element from Scratch
- Encapsulating Style and Structure with Shadow DOM
- Advanced Tooling for Web Components
HTML templates
One of the least recognized, but most powerful features of the Web Components specification is the <template>
element. In the first article of this series, we defined the template element as, “user-defined templates in HTML that aren’t rendered until called upon.” In other words, a template is HTML that the browser ignores until told to do otherwise.
These templates then can be passed around and reused in a lot of interesting ways. For the purposes of this article, we will look at creating a template for a dialog that will eventually be used in a custom element.
Defining our template
As simple as it might sound, a <template>
is an HTML element, so the most basic form of a template with content would be:
<template>
<h1>Hello world</h1>
</template>
Running this in a browser would result in an empty screen as the browser doesn’t render the template element’s contents. This becomes incredibly powerful because it allows us to define content (or a content structure) and save it for later — instead of writing HTML in JavaScript.
In order to use the template, we will need JavaScript
const template = document.querySelector('template');
const node = document.importNode(template.content, true);
document.body.appendChild(node);
The real magic happens in the document.importNode
method. This function will create a copy of the template’s content
and prepare it to be inserted into another document (or document fragment). The first argument to the function grabs the template’s content and the second argument tells the browser to do a deep copy of the element’s DOM subtree (i.e. all of its children).
We could have used the template.content
directly, but in so doing we would have removed the content from the element and appended to the document’s body later. Any DOM node can only be connected in one location, so subsequent uses of the template’s content would result in an empty document fragment (essentially a null value) because the content had previously been moved. Using document.importNode
allows us to reuse instances of the same template content in multiple locations.
That node is then appended into the document.body
and rendered for the user. This ultimately allows us to do interesting things, like providing our users (or consumers of our programs) templates for creating content, similar to the following demo, which we covered in the first article:
See the Pen
Template example by Caleb Williams (@calebdwilliams)
on CodePen.
In this example, we have provided two templates to render the same content — authors and books they’ve written. As the form changes, we choose to render the template associated with that value. Using that same technique will allow us eventually create a custom element that will consume a template to be defined at a later time.
The versatility of template
One of the interesting things about templates is that they can contain any HTML. That includes script and style elements. A very simple example would be a template that appends a button that alerts us when it is clicked.
<button id="click-me">Log click event</button>
Let’s style it up:
button {
all: unset;
background: tomato;
border: 0;
border-radius: 4px;
color: white;
font-family: Helvetica;
font-size: 1.5rem;
padding: .5rem 1rem;
}
…and call it with a really simple script:
const button = document.getElementById('click-me');
button.addEventListener('click', event => alert(event));
Of course, we can put all of this together using HTML’s <style>
and <script>
tags directly in the template rather than in separate files:
<template id="template">
<script>
const button = document.getElementById('click-me');
button.addEventListener('click', event => alert(event));
</script>
<style>
#click-me {
all: unset;
background: tomato;
border: 0;
border-radius: 4px;
color: white;
font-family: Helvetica;
font-size: 1.5rem;
padding: .5rem 1rem;
}
</style>
<button id="click-me">Log click event</button>
</template>
Once this element is appended to the DOM, we will have a new button with ID #click-me
, a global CSS selector targeted to the button’s ID, and a simple event listener that will alert the element’s click event.
For our script, we simply append the content using document.importNode
and we have a mostly-contained template of HTML that can be moved around from page to page.
See the Pen
Template with script and styles demo by Caleb Williams (@calebdwilliams)
on CodePen.
Creating the template for our dialog
Getting back to our task of making a dialog element, we want to define our template’s content and styles.
<template id="one-dialog">
<script>
document.getElementById('launch-dialog').addEventListener('click', () => {
const wrapper = document.querySelector('.wrapper');
const closeButton = document.querySelector('button.close');
const wasFocused = document.activeElement;
wrapper.classList.add('open');
closeButton.focus();
closeButton.addEventListener('click', () => {
wrapper.classList.remove('open');
wasFocused.focus();
});
});
</script>
<style>
.wrapper {
opacity: 0;
transition: visibility 0s, opacity 0.25s ease-in;
}
.wrapper:not(.open) {
visibility: hidden;
}
.wrapper.open {
align-items: center;
display: flex;
justify-content: center;
height: 100vh;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 1;
visibility: visible;
}
.overlay {
background: rgba(0, 0, 0, 0.8);
height: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
}
.dialog {
background: #ffffff;
max-width: 600px;
padding: 1rem;
position: fixed;
}
button {
all: unset;
cursor: pointer;
font-size: 1.25rem;
position: absolute;
top: 1rem;
right: 1rem;
}
button:focus {
border: 2px solid blue;
}
</style>
<div class="wrapper">
<div class="overlay"></div>
<div class="dialog" role="dialog" aria-labelledby="title" aria-describedby="content">
<button class="close" aria-label="Close">✖️</button>
<h1 id="title">Hello world</h1>
<div id="content" class="content">
<p>This is content in the body of our modal</p>
</div>
</div>
</div>
</template>
This code will serve as the foundation for our dialog. Breaking it down briefly, we have a global close button, a heading and some content. We have also added in a bit of behavior to visually toggle our dialog (although it isn’t yet accessible). Unfortunately the styles and script content aren’t scoped to our template and are applied to the entire document, resulting in less-than-ideal behaviors when more than one instance of our template is added to the DOM. In our next article, we will put custom elements to use and create one of our own that consumes this template in real-time and encapsulates the element’s behavior.
See the Pen
Dialog with template with script by Caleb Williams (@calebdwilliams)
on CodePen.
Article Series:
- An Introduction to Web Components
- Crafting Reusable HTML Templates (This post)
- Creating a Custom Element from Scratch
- Encapsulating Style and Structure with Shadow DOM
- Advanced Tooling for Web Components
What I’ve been doing recently is instead of writing styles in the
<style>
tag inside the template, I’ve been using<style>@import '/path/to/component.css';</style>
.So that way, I could maintain stylesheets easily, and also have other stylesheets included from there, like common global styles that you want to add that cannot directly pierce the shadowDOM.
Hey Laxman, this is a perfectly legitimate strategy and one that makes a lot of sense if you are absolutely certain that you know the path to your styles. For this example, however, I wanted to make sure that the very basics were covered without getting too deep into the best way to import styles (something I’ll talk more about in the shadow DOM article).
Are style and script element inside templates scoped in any way? Or do they just become additional style and scripts in the document as a whole, subject to the usual cascade rules for the style, and inserted into the same javascript namespace for scripts? If unscoped, functions in the script would be duplicated once per template use, and would overwrite the names of the previous copy each time? And the styles would keep stacking up in the cascade. Neither of which would make style and script in multi-use templates particularly attractive.
Hey Glenn, thanks for asking. No, style and script elements inside of a template are not scoped so using the element this was is really not that great of a strategy. The template node is really more for HTML rather than styles or scripts, but I wanted to demonstrate that they can be used this way. In the next two articles we’ll discuss how to further optimize this code making use of custom elements and shadow DOM.
Nice series. I really like that you are able to explain it as briefly as possible, but without omitting the main things. One thing that is bothering though. Is there any specific reason why you use
document.importNode()
and notNode.cloneNode()
(e.g.fragment.content.cloneNode(true);
) ?Plese do keep up.
So there’s not really much of a difference between the two. If I’m not mistaken, I think using
cloneNode
would implicitly adopt the node if the documents are different (and they would be as the template node is a document fragment). So in that case,document.importNode
is more explicit.Very nice article! Does anyone a Template Engine Implementation with the HTML Element template infrastructure? It would also be interesting to load such Templates asynchron later to your website or is that verbose?
I have a small question I am experienced in front-end development & I’m building my web site in HTML/CSS with bootstrap for a framework. What I would like to do is setup my Main page elements: Top Nav Bar, Footer Etc. into templates and call them on every page. Would these web components be a good way to do that or is there an easier/less steps to accomplish this even by using Javascript?