Link Search Menu Expand Document

Module 3: HTML Templates

In this module, we’re going to look at another of the core technologies behind web components - HTML templates.

⚠ Be careful if you’re looking this up online; there are literally thousands of web pages out there which use the phrase “HTML templates” in various context, because we’ve been using HTML to create various kinds of templated content since the earliest days of the web. Here, we are specifically talking about the HTML <template> element, which was introduced as part of HTML 5.

Introducing the template element

HTML5 introduced a new HTML element called template, which is for defining content that should be parsed but not rendered. When the browser encounters a template element, it will parse the contents of that element, but it won’t actually render anything to the screen. It won’t evaluate any scripts, it won’t load any external resources referenced tags like link or img – but it will create an in-memory DOM tree.

The main advantage of using template elements is performance; because the browser has already parsed the markup, it’s significantly quicker to clone content from a template and insert the cloned content into your page than it is to use innerHTML or other DOM manipulation methods.

In other words - template is a place to create and store HTML that you’re going to use later. Rafael Weinstein, who worked on the spec for HTML templates, described them as “a place to put a big wad of HTML that you don’t want the browser to mess with at all…for any reason.”

Sure, there are all sorts of ways to put content in your page and hide it from the browser until you’re ready to use it: CSS rules like display: none or visibility: hidden; using negative margins to render content off-screen; setting width and height to 0px. But they’re all kinda hacky, and none of them is really what we’re after. Take a look at the Minions example minions.html to see the difference between hiding markup using CSS and wrapping it in a template.

Populating templates using JavaScript

Here’s an example of a page that uses the template element:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Playlists!</title>
    <link type="text/css" rel="stylesheet" href="playlist.css" />
</head>

<body>
    <h4>Let's Make A Playlist!</h4>
    <table>
        <thead>
            <tr>
                <th>Artist</th>
                <th>Title</th>
                <th>Album</th>
                <th>Search</th>
            </tr>
        </thead>
        <tbody id="playlist-body">
            <!-- the table body will be populated by our JavaScript code -->
        </tbody>
    </table>
    <template id="playlist-entry-template">
        <tr>
            <td id="artist-td"></td>
            <td id="song-td"></td>
            <td id="album-td"></td>
            <td><a id="search-link">search</a></td>
        </tr>
    </template>
    <form>
        <p>Add a song to the playlist:</p>
        <label for="artist-input">Artist:</label> <input type="text" name="artist" id="artist-input" /><br />
        <label for="song-input">Song:</label> <input type="text" name="song" id="song-input" /><br />
        <label for="album-input">Album:</label> <input type="text" name="album" id="album-input" /><br />
        <input type="button" value="Add to Playlist" onclick="addSongToPlaylist(this.form);" />
    </form>
    <script type="text/javascript" src="playlist.js"></script>
</body>
</html>

Try it live: playlist.html

There’s a lot going on here, so let’s break it down into sections:

First, take a look at the template element:

<template id="playlist-entry-template">
    <tr>
        <td id="artist-td"></td>
        <td id="song-td"></td>
        <td id="album-td"></td>
        <td><a id="search-link">search</a></td>
    </tr>
</template>

In regular HTML, a table row tr tag isn’t valid anywhere except inside a table – but that restriction doesn’t apply to templates. We’ve also given each table data cell td tag an id so we can refer back to it later.

Now for the code that runs when we click the button:

function addSongToPlaylist(form) {
    // Read input values from the form
    let artist = form["artist"].value;
    let song = form["song"].value;
    let album = form["album"].value;
    let searchText = (artist + ' ' + song + ' ' + album).toLowerCase().replace(' ', '+');
    let searchHref = `https://www.google.com/search?q=${searchText}`;

    // get a reference to the template and create a clone (copy) of it.
    let template = document.getElementById('playlist-entry-template').content.cloneNode(true);

    // Populate the template with values from the form.
    template.getElementById('artist-td').innerText = artist;
    template.getElementById('song-td').innerText = song;
    template.getElementById('album-td').innerText = album;
    template.getElementById('search-link').setAttribute('href', searchHref);

    // Add the new table row content to the table body
    let tbody = document.getElementById('playlist-body');
    tbody.appendChild(template);
}

The really important part here is this line:

let template = document.getElementById('playlist-entry-template').content.cloneNode(true);

See, we’re not actually manipulating the template that’s on the page – we’re making a copy of it (using cloneNode), populating that copy with data from our form, and then connecting the copy to the DOM using appendChild. This way, we can keep reusing the same template over and over again: make a copy with cloneNode, populate it, use appendChild to add it to the document, and repeat as required.

Populating templates using slots

In the example above, we used JavaScript to inject content into our template by setting the innerText and innerHTML property of elements within our template. But… with regular HTML tags, we don’t need to use JavaScript to populate content. We can just wrap our content up in the tag, right?

By combining HTML templates with custom elements, we can do the same thing with our own components.

Here’s what we want to be able to do:

<big-red-title>Look! This is a big red title</big-red-title>

And here’s the code that makes it possible:

class BigRedTitleElement extends HTMLElement {
    constructor() {
        super()
        let template = document.createElement('template');
        template.innerHTML = `<slot style="font-size: 200%; color: red;"></slot>`;
        let node = template.content.cloneNode(true);
        this.attachShadow({ mode: 'open' }).appendChild(node);
    }
}
customElements.define("big-red-title", BigRedTitleElement);

Try it live: big-red-title.html

Notice in this example how the template is created inside the component’s constructor, instead of being included somewhere in the hosting page; this makes it easier to reuse the component across different pages because all we need to include is the JavaScript file.

In this example, we’ve got a single <slot> inside our template, and any content (including HTML) that’s inside our <big-red title></big-red-title> tag will be rendering in place of this slot.

Working with named slots

Templates can include multiple slots, providing more detailed control over how our content is rendered by our components. A named slot is a placeholder that will be replaced with a chunk of HTML when the templated component is rendered. Here’s an example of a template that includes named slots:

<template>
    <div class="forum-post">
        <div class="avatar-image">
        <slot name="avatar-image"></slot>
        </div>
        <p><slot></slot></p>
        <span class="forum-post-user-info">posted at <slot name="post-date"></slot> by <slot name="post-user"></slot>
    </div>
</template>

and here’s how you’d instantiate the associated component on your page. Notice how the slot attributes on elements like <a slot="post-date">content</a> need to match the name attributes on the slot elements in the template: <slot name="post-date></slot>

<forum-post>
    This is an example of a forum post that just includes some simple text.
    <img slot="avatar-image" src="jerry-minion.png" />
    <a slot="post-date">15:52 on 5 January 2021</a>
    <a slot="post-user" href="/users/jerry">Jerry Minion</a>
</forum-post>

<forum-post>
    This forum post includes some HTML:
    <ul>
        <li>Look, it's got bullet points!</li>
        <li>Isn't that amazing?</li>      
    </ul>
    <p>There is even a <a href="https://example.com">link</a> in the post body. Isn't that cool?</p>
    <img slot="avatar-image" src="jerry-minion.png" />
    <a slot="post-date">15:52 on 5 January 2021</a>
    <a slot="post-user" href="/users/jerry">Jerry Minion</a>
</forum-post>

Take a look at forum-posts.html for a full example of a page that uses templates, named slots and the shadow DOM.

💭 Named slots are an interesting technology, but when I was creating this tutorial, I couldn’t find any really compelling use cases for named slots. They exist, and they work in most browsers, but I couldn’t find a really good example or any real-world scenarios where named slots provided a huge improvement over using a template element and manipulating the templated elements using .innerHTML. It’s definitely worth knowing about them, and understanding how they work, but if you can’t think of a good reason to use them, you’re not alone 🙂

HTML Templates: Review

  • The HTML <template> element is a container for reusable markup that we’re going to use later.
  • Browsers will parse content inside template elements, but won’t render it - scripts don’t run, images won’t download, embedded media won’t play.
  • You can add <template> elements to your page markup, or create them in script using document.createElement('template')
  • The <slot> element can be used to create placeholders inside templates
  • When using templates with custom elements, the custom element markup can use slot="name" to target a named slot in a template.

Resources and Further Reading