HTML5 Templates From Frameworks To Benchmarks pt. 3

December 16, 2018 - Sam Messina

In my previous post, I talked about how to use JavaScript template literals to quickly and cleanly compose dynamic JavaScript string and DOM nodes, but how do we inject those into the DOM without innerHTML calls hijacking the performance gains we got from using template literals?

Template elements, of course!

This is part three of a three part series. Check out Part 1: The Problem Introduction, and Part 2: JavaScript Template Literals as well.

History and Context

Template elements are 1/4 of what makes up the Web Component ecosystem. Web components in general are still in the very early stages of developement, but template elements are already available in the living standard and can be used today in tandem with JavaScript template literals.

Basics

Template elements are a way to load DOM content with less rendering overhead. The HTML placed between template tags will be parsed for validity, but will not be rendered until it is manually loaded into the DOM. This means all string concatenation, script loading, image rendering, etc. are only evaluated when we load the template element into DOM.

But, what does this solve?

Template literals got us dynamic data at much faster speeds, but HTML5 template elements allow us to inject that new HTML into our page without fear of Cross Site Scripting or ugly hacks like a display: none placeholder element. Now, our placeholders are baked right into the HTML specifications!

Problems

Let’s dig into some code.

We’re going to create an HTML header that greets users with “Good morning <name>,” “Good afternoon <name>,” or “Good evening <name>,” depending on the time of day. Simple enough, right?

Let’s start with our JavaScript code to simply get the appropriate text:


let name = "Sam Messina"
let date = new Date();
let hour = date.getHours();
let time_of_day;

if (hour < 12) { time_of_day = "morning"; }
else if (hour < 18) { time_of_day = "afternoon"; }
else { time_of_day = "evening"; }

let personal_greeting = `Good ${time_of_day}, ${name}!`;

So we can now inject our string into HTML. Traditionally, this might look something like this:


<header>
  <h1>Welcome!</h1>
  <h2 id="personal-greeting"></h2>
</header>
<script type="text/javascript">
  let name = "Sam Messina"
  let date = new Date();
  let hour = date.getHours();
  let time_of_day;

  if (hour < 12) { time_of_day = "morning"; }
  else if (hour < 18) { time_of_day = "afternoon"; }
  else { time_of_day = "evening"; }

  let personal_greeting = `Good ${time_of_day}, ${name}!`;

  document.getElementById("personal-greeting").innerHTML = personal_greeting;
</script>

There are a few things that should seem smelly with the above code. Firstly, is the capacity for Cross Site Scripting.

MDN points out that even though script tags cannot be inserted via .innerHTML, there are still methods of executing JavaScript through an .innerHTML call, opening up potential for a Cross Site Scripting attack.

MDN suggests using Node.textContent to instead insert plain text rather than HTML. Let’s see what that looks like…


<header>
  <h1>Welcome!</h1>
  <h2 id="personal-greeting"></h2>
</header>
<script type="text/javascript">
  let name = "Sam Messina"
  let date = new Date();
  let hour = date.getHours();
  let time_of_day;

  if (hour < 12) { time_of_day = "morning"; }
  else if (hour < 18) { time_of_day = "afternoon"; }
  else { time_of_day = "evening"; }

  let personal_greeting = `Good ${time_of_day}, ${name}!`;

  document.getElementById("personal-greeting").textContent = personal_greeting;
</script>

Ok, not super robust. But it does work so far…

Another smell with the above code is the fact that we have an empty h2 tag sitting there waiting for content. An obvious solution is to create the element in JavaScript directly and append it to the markup.


<header>
  <h1>Welcome!</h1>
</header>
<script type="text/javascript">
  let name = "Sam Messina"
  let date = new Date();
  let hour = date.getHours();
  let time_of_day;

  if (hour < 12) { time_of_day = "morning"; }
  else if (hour < 18) { time_of_day = "afternoon"; }
  else { time_of_day = "evening"; }

  let personal_greeting = `Good ${time_of_day}, ${name}!`;

  // create the h2 element
  let h2_element = document.createElement("h2")

  // create a text node to append to the h2 element
  let personal_greeting = document.createTextNode(personal_greeting);

  // add the text to the h2 greeting
  h2_element.appendChild(personal_greeting);

  // append the h2 greeting to our header
  document.querySelector("header").appendChild(h2_element);
</script>

Sure, this works. But it’s super gross, the JavaScript and rendering overhead is high, and for non-trivial element composition, this gets unmaintainable. Remember, this is a simple use case we’re dealing with. Can you imagine doing this for nested elements of non-trivial data? Ugh.

Additionally, what happened to the sleek syntax of our JavaScript template literals?! They’ve been totally overshadowed by ugly document calls.

Solutions

You’ve probably guessed it, but template elements are our solution.

Template elements allow us to combine the best parts of all of the above solutions: - Since they’re not rendered, we can keep our placeholder h2 element. - Since they are injected as DOM nodes automatically, we don’t have to worry about Cross Site Scripting attacks. - Since we can use regular strings, our template literals can be used to their full potential.

Woot!

Usage

Working with template elements has a few steps.

1. Add the template element to your HTML markup

First thing you need is a template element to work with! For our use case, that would look something like this:


<header>
  <h1>Welcome!</h1>
</header>

<template id="personal-greeting">
  <h2></h2>
</template>

Note that this template is parsed, but not rendered by the browser. It’s just a placeholder for content to come.

2. Query for the template using JavaScript

Once we have the HTML in place, we can retrieve the template element using whatever API you prefer (I’ll be using the document.querySelector() API here).


<header>
  <h1>Welcome!</h1>
</header>

<template id="personal-greeting">
  <h2></h2>
</template>

<script type="text/javascript">
  // get the template
  let template = document.querySelector("#personal-greeting");
</script>

3. Add your dynamic content to the template

To add content to a template element, you need to extract its own content. This retrieves everything inside the template element, but not the template element itself.


<header>
  <h1>Welcome!</h1>
</header>

<template id="personal-greeting">
  <h2></h2>
</template>

<script type="text/javascript">
  // get the template
  let template = document.querySelector("#personal-greeting");

  // extract the contents of the template to populate
  let templateContents = document.importNode(template.content, true);

  // populate the contents of the template
  let h2_element = templateContents.querySelector("h2");
  h2_element.textContent = "It Works!";
</script>

4. Inject the template into your markup

The last thing to do is append your new node to the proper location. We can do this with a simple appendChild() call.


<header>
  <h1>Welcome!</h1>
</header>

<template id="personal-greeting">
  <h2></h2>
</template>

<script type="text/javascript">
  // get the template
  let template = document.querySelector("#personal-greeting");

  // extract the contents of the template to populate
  let templateContents = document.importNode(template.content, true);

  // populate the contents of the template
  let h2_element = templateContents.querySelector("h2");
  h2_element.textContent = "It Works!";

  // get the parent element
  let header = document.querySelector("header");

  // append the new element to the parent
  header.appendChild(templateContents);
</script>

Hooking it up with Template Literals


<header>
  <h1>Welcome!</h1>
</header>

<template id="personal-greeting">
  <h2></h2>
</template>

<script type="text/javascript">
  // get the template
  let template = document.querySelector("#personal-greeting");

  // extract the contents of the template to populate
  let templateContents = document.importNode(template.content, true);

  // populate the contents of the template
  let h2_element = templateContents.querySelector("h2");
  h2_element.textContent = "It Works!";

  // get the parent element
  let header = document.querySelector("header");

  // append the new element to the parent
  header.appendChild(templateContents);
</script>

Resources, credit, and thanks