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
- Dr. Axel Rauschmayer’s book: Exploring JS has a full chapter on template literals
- Moxilla’s Developer Network has its own entry on template literals.
- React code was taken from Facebook’s "Why not template literals? post.
- all framework sizes and performance data was taken from Bundle Phobia and JS Perf