HTML templating with vanilla JavaScript ES2015 Template Literals
I needed to prototype something recently by creating output HTML from a modest set of data. I wanted to avoid extra dependencies such as Handlebars and managed to get the job done using ES2015 Template Literals.
They are more powerful than I first thought so, I wanted to take the time to document what I discovered.
Let’s look at the power of Template Literals and how we can perform nested loops inside them.
Existing documentation
I found a few posts and Stack Overflow threads on the subject of Template Literals. Of note were:
I’m not going to cover everything in the prior posts. Instead I want to limit our time here to show you how you can create HTML using Template Literals to based on the following kind of data. Note, this is entirely contrived data so don’t get too hung up on the content!
var data = [
{
Title: "The OA",
Ended: false,
Description:
"Started with promise but then you watch the final episode and realise it is tosh",
Episodes: [
"Homecoming",
"New Colossus",
"Champion",
"Away",
"Paradise",
"Forking Paths",
"Empire of Light",
"Invisible Self"
],
Reviews: [
{ "Rotten Potatos": 69 },
{ "Winge Central": 48 },
{ "Fan Base Fanatics": 62 },
],
UserRatings: [7,3,7,8,9,2,4,5,7,6],
},
{
Title: "Lost",
Ended: true,
Description:
"The instigator of the whole, 'keep them guessing and we'll keep making stuff up as we go' style drama",
Episodes: [
"Pilot (Part 1)",
"Pilot (Part2)",
"Tabula Rasa",
"Walkabout",
"White Rabbit",
"House of the Rising Sun",
"The Moth",
"Confidence Man",
],
Reviews: [
{ "Rotten Potatos": 62 },
{ "Winge Central": 46 },
{ "Fan Base Fanatics": 72 },
],
UserRatings: [2,8,8,8,7,6,7,8,10,6],
},
];
In the above JavaScript object I have 2 ‘records’ of a data set. For each record I want output some HTML to produce something simple like this:
See the Pen Playing with ES2015 Template Literals by Ben Frain (@benfrain) on CodePen.
You can also look at the completed example on CodePen:
We are going to achieve this by not just stamping out the data as is, but also leveraging the ability of Template Literals to do if/else switching and also calling out to other functions to do ‘stuff’ such as averaging a bunch of numbers.
Let’s begin.
Template Literals basics
Until ES2015, if you wanted to create a string in JavaScript from distinct pieces, for example, an existing string plus a number, it was a little messy. For example:
var result = 10;
var prefix = "the first double digit number I learnt was ";
var assembled = prefix + result.toString();
console.log(assembled); // logs => 'the first double digit number I learnt was 10'
ES2015 introduced ‘Template Literals’. They allow us to do this instead:
var result = 10;
var assembled = `the first double digit number I learnt was ${result}`;
console.log(assembled); // logs => 'the first double digit number I learnt was 10'
The back ticks define the contents as a template literal and then we can interpolate values from elsewhere in JavaScript using the ${}
pattern. Anything inside the curly braces, which in turn are within the backticks, are evaluated and then inserted as a string.
In order to generate our HTML for each data record, as our data is an Array, we will start with a forEach
.
var data = [
// Data here
];
// loop through the data
data.forEach((datarecord, idx) => {
// for each record we call out to a function to create the template
let markup = createSeries(datarecord, idx);
// We make a div to contain the resultant string
let container = document.createElement("div");
container.classList.add("as-Series");
// We make the contents of the container be the result of the function
container.innerHTML = markup;
// Append the created markup to the DOM
document.body.appendChild(container);
});
function createSeries(datarecord, idx) {
return `
<div class="a-Series_Title">${datarecord.Title}</div>
`;
}
I opted to call out to another function to generate the markup. The createSeries
function simply receives data and returns a string (the completed template) with all the blanks filled in from the data.
Here is a fuller initial version of the template:
function createSeries(datarecord, idx) {
return `
<h2 class="a-Series_Title">${datarecord.Title}</h2>
<p class="a-Series_Description">
<span class="a-Series_DescriptionHeader">Description: </span>${datarecord.Description}
</p>
<div class="a-EpisodeBlock">
<h4 class="a-EpisodeBlock_Title">First episodes</h4>
</div>
`;
}
With that in place, we get each record title wrapped inside our div. That’s the majority of what you need to know to use template literals for templates.
Looping within a Template Literal
However, I am now at the point where I need to loop within my Template Literal. I have an array inside my Episodes
key that I want to iterate through so I can print out the names of each episode of a season. Thankfully, we can use the map
method to do this. I’ve talked about Array.map
before but to recap, it creates a new array which is the result of mapping a function onto each element in the existing array. In terms of our template, let’s pick up where we left off above and use map
to spit out our episodes. The syntax of the map, inside an existing template literal, will look like this:
${THINGTOLOOP.map(item, idx) => `Your looping code here with data inserted like ${this}`}
So, in our template, I’m adding it like this:
function createSeries(datarecord, idx) {
return `
<h2 class="a-Series_Title">${datarecord.Title}</h2>
<p class="a-Series_Description">
<span class="a-Series_DescriptionHeader">Description: </span>${datarecord.Description}
</p>
<div class="a-EpisodeBlock">
<h4 class="a-EpisodeBlock_Title">First episodes</h4>
${datarecord.Episodes.map((episode, index) =>
`<div class="a-EpisodeBlock_Episode">
<b class="">${index+1}</b>
<span class="">${episode}</span>
</div>
`
)}
</div>
`};
That’s great, all printing out spot on! Oh, wait! What’s with the comma after each episode title?
Episodes
1 Homecoming
,
2 New Colossus
,
3 Champion
,
4 Away
,
5 Paradise
,
6 Forking Paths
,
7 Empire of Light
,
8 Invisible Self
It turns out that as the Template Literal is returning a string from the Array, it is joining the contents together with a comma by default. Thank goodness for stack overflow! We can join the looped items with nothing instead by appending .join("")
at the end of the map
method, for example:
${datarecord.Episodes.map((episode, index) =>
`<div class="a-EpisodeBlock_Episode">
<b class="">${index+1}</b>
<span class="">${episode}</span>
</div>
`
).join("")}
That sorts that issue!
Now, the data for our reviews is in a different ‘shape’ as they are keys within objects but we can still use them in the Template Literal with a map
like this:
${datarecord.Reviews.map(review =>
`<div class="a-ReviewsBlock_Episode">
<b class="a-ReviewsBlock_Reviewer">${Object.keys(review)[0]}</b>
<span class="a-ReviewsBlock_Score">${review[Object.keys(review)[0]]}%</span>
</div>`
).join("")}
Our next challenge is how to perform if/else logic inside the template.
How to perform if/else switches with ternary operators
Inside our template literal I want to add a ‘More to come!’ message if the series is still in production. We will use the Ended
boolean inside the data as our ‘test’ to show this or not and we will employ a ternary operator to perform the test. If you don’t know what a ternary operator is, it is a concise form of if/else statement. It looks like this:
thingToTest === true ? doThisIfTrue : otherWiseDoThis;
So, the first part is the ‘test’, the if
. Then after that test is the ?
which is where we then specify the code to run if our test is true. Then there is the :
which is the break for the else
part of the logic. We run the code after that if the test result is false. You know, it’s probably easier to just show you how this would work in our template:
${datarecord.Ended === true ? `` : `<div class="a-Series_More">More to come!</div>`}
So, in this case, if that key is true, we are returning an empty string, otherwise we return the message. Again this is inside an existing template literal.
Using the returned value of another function
With a template literal it is possible render in part of the resultant string by way of another function. So, in our example, suppose we want to render out an average of the scores received by users. That data is held in the UserRatings
array which looks like this:
UserRatings: [2,8,8,8,7,6,7,8,10,6],
We will create a function(s) to sum the array and then divde by the length of the array to get an average:
const add = (a, b) => a + b;
function getRatingsAverage(ratings) {
return ratings.reduce(add) / ratings.length;
}
Now to use this inside our template we can do this:
`<div class="a-UserRating">Average user rating: <b class="a-UserRating_Score">${getRatingsAverage(datarecord.UserRatings)}</b></div>`
Security
At stated at the top, I’ve been using this technique for local prototyping. However, since publishing, a few people, such as Tommy Carlier below have pointed out some inherent security concerns if you are considering using this kind of approach in production or anywhere you can’t guarantee the safety of the input data.
For example, suppose we were going to accept user input from an input
element to interpolate into our template. In that instance there is nothing to stop somebody adding some malicious script. Imagine here the username has been input from a form (thanks to Craig Francis for the example):
var username = 'craig<script>alert("XSS")</' + 'script>';
document.write(`<p>Hi ${username}</p>`);
The solution here is to ‘escape’ the HTML or use the textContent
method to ensure the inserted strings are properly escaped. If that’s a scenario you find yourself in, there is a good section in the Google documentation on Template Literals here: https://developers.google.com/web/updates/2015/01/ES6-Template-Strings
Justin Fagnani, who works on Polymer at Google also pointed me to their ‘lit-html’ library. That takes care of these issue too and currently weighs in at 2.2Kb so I’d definitely look at that in future if heading down that path.
Conclusion
Template Literals afford a lot of power with no library overhead. I will definitely continue to use them when complexity of handlebars or similar is overkill.
The only downsides I found related to developer ergonomics. Prettier currently mashes up the contents of Template Literals pretty badly. I’m confident that will get addressed but right now it’s not a great experience; it can be quite gnarly trying to spot which tags start and end where.
Also, syntax highlighting of markup inside the template strings isn’t great (in Sublime or VS Code at least). Like the Prettier grumble, this doesn’t effect output but makes the experience less pleasurable.
Ultimately I’ve been pleasantly surprised at the versitility of Template Literals and hope this post provides enough curiosity for you to give them a whirl yourself.
I think it’s important to note that all data that ends up in the template should be escaped, otherwise HTML-code (including potentially malicious scripts) could be injected in your page.