I had a recent discussion with one of the awesome developers at the FT - @bjfletcher. We were looking at how viable it would be to replace a templating language, like Handlebars with ES6 Template Literals, in some manner. Ben suggested it'd be a good idea to turn our discussion into a post, and here we are - with a click bait title and everything.
So what are Template Literals? How can they do what a complex library like Handlebars does? Settle in, dear reader, and let's find out...
A crash course through Template Literals
I've discussed ES6 here before - we looked at Symbols (you should read that, we'll be using Symbols today), Reflect and we looked at Proxies. Even more ES6 content will be coming too (tease tease). Another big part of ES6 was also these things called Template Literals. They serve as a bit of a fix for many things to do with Strings in ES6 land. So let's just have a quick rundown of their features:
A literal syntax
ES6 template literals, as the name suggests, come with a new literal syntax. They use back ticks (` `
) to denote the start and end (just like strings use quote marks (' '
or " "
)). Within those back ticks, Template literals are parsed at runtime, to provide some new features.
Newlines allowed.
One of the neat little features about Template Literals, is that unlike String literals, they allow for newline characters; strings spanning over multiple lines look a lot cleaner as you don't need to write a bunch of ugly syntax concatenating them over lines. This seems like a small, maybe irrelevant feature, but it's very important for our use case - making template languages! Here's an example:
var templateString = `
Hello
World
`;
// equivalent with strings:
var oldString = '\n' +
' Hello\n' +
' World\n';
Expression interpolation
The most powerful feature of Template Literals, is that they have a new syntax inside the string which allows inline code within that. Simply wrap any code within ${
and }
and the code inside will be executed, and added to the string in place. If you've used Ruby before - this concept will be familiar to you (Ruby has a practically identical feature using #{
and }
instead). This is the first time JS has been able to do this. Here's a Ruby example:
puts "1+1 = #{ 1 + 1 }" # Outputs: "1+1 = 2"
Here's the same kind of code, written in ES5 (Old JS) code:
console.log('1+1 = ' + (1 + 1)); // Outputs: "1+1 = 2"
Now here's an example using ES6 code - this is what we can do with the amazing new Template Literal expression interpolation features!
console.log(`1+1 = ${ 1 + 1 }`); // Outputs: "1+1 = 2"
In this string substitution, the code (1 + 1
) is executed, and the value (2
) is coerced to a string ("2"
) and then added to the output string in place. We can also make use of variables here - they work in the scopes you'd expect them to:
var place = 'World';
console.log(`Hello ${place}`); // Outputs: "Hello World"
function greeting(place) {
return `Hello ${place}`;
}
console.log(greeting('readers')); // Outputs: "Hello readers"
This ability for interpolation gives us some great abilities to create functions, which can take data, and output a string - which is the fundamentals of any templating language! However, there's a few missing pieces which we still need; things like the ability to control the data behind the interpolations, which is critical for escaping bad code (a core feature of any good templating language). What a nice segue into...
Tagged template literals
This is the last important feature we'll be discussing about Template Literals. Tagged Template Literals provide a way to expose the component parts of a Template Literal to a function. Template Tags are just normal functions, but to be useful they have to be invoked differently. If you try to call a function with a Template Literal in the normal way you'll just get a String as the first argument. If you call it in the special Tagged Template Literal way, then you'll get the composite parts of the template literal; you'll get access to all the string fragments, and all of the results from each interpolated expression.
Alright, all of that is a bit dense, so let's look at some code to illustrate:
// here is our normal function. It takes two arguments, `strings` and `value1`
function greeting(strings, value1) {
console.log('Strings:', strings);
console.log('Value1:', value1);
}
var place = 'World';
// If we try to call our function in the normal way, we're really just passing a string as the first argument
greeting(`Hello ${place}`);
// Log output:
// Strings: 'Hello World'
// Value1: undefined
// invoking our function as a Tagged Template Literal - note the lack of parenthesis
greeting`Hello ${place}`
// Log output:
// Strings: ['Hello ', '']
// Value1: 'World'
Notice that when calling greeting()
the normal way, with a template string as the first argument - the string is fully rendered
("Hello World"
) and that is passed to greeting()
, but when we remove the parens - making it a Tagged Template Literal - we get an Array of all of the Strings (['Hello ', '']
) as the first argument, and the second argument is our interpolated place
value ('World'
). These are each of the component parts of the template - we can put these all together to remake the String, just as you'd expect:
function greeting(strings, value1) {
return strings[0] + value1 + strings[1];
}
var place = 'World';
console.log(greeting`Hello ${place}`)); // Outputs: "Hello World"
Now each Expression Interpolation in a Template Literal is passed as a new argument to the function. This means if we want to handle all possible values, we need to make a variadic function (which is just a fancy way to say it takes an unlimited amount of arguments). Also every time there is an Expression Interpolation there will be 2 Strings in the strings
array - one for the left side of the interpolation, the second for the right side of the interpolation. So if the Template Literal has 1 Expression Interpolation then the tag function will have an array of 2 strings, and a second argument. If the Template Literal has 2 expressions, then the tag function will have an array of 3 strings, and a second and third argument.
To make a function which we can reuse with a variety of Template Literals with or without Expression Interpolation, we ideally just want to loop through all of the String values and concatenate them with all of the values. Let's write some code for this!
function getFullString(strings) {
var interpolatedValues = [].slice.call(arguments, 1); // get the "variadic" values from arguments, make them an array
return strings.reduce(function (total, current, index) { // use `reduce` to iterate over the array, returning a new value
total += current; // append the string to the total string
if (interpolatedValues.hasOwnProperty(index)) { // if there is an interpolatedValue with a matching index, append this too
total += interpolatedValues[index];
}
return total;
}, ''); // the starting value is an empty string (`''`)
}
var place = 'World';
getFullString`Hello ${place}!`; // outputs: "Hello World!"
We can use a few ES6 tricks to shorten the above function, into something a bit more compact and readable:
function getFullString(strings, ...interpolatedValues) { // `...` essentially slices the arguments for us.
return strings.reduce((total, current, index) => { // use an arrow function for brevity here
total += current;
if (interpolatedValues.hasOwnProperty(index)) {
total += interpolatedValues[index];
}
return total;
}, '');
}
Of course this getFullString
function doesn't do anything special, that Template Literals don't already do. I mentioned previously though, that a core function of templating languages like Handlebars is the automatic escaping input. HTML templating languages like Handlebars will escape any variables injected in the template, to prevent XSS (Cross Site Scripting) vulnerabilities - basically ensuring the injected variables cannot render new HTML, like a <script>
tag. We can do the same kind of escaping with our tag function:
function safeHTML(strings, ...interpolatedValues) { // `...` essentially slices the arguments for us.
return strings.reduce((total, current, index) => { // use an arrow function for brevity here
total += current;
if (interpolatedValues.hasOwnProperty(index)) {
total += String(interpolatedValues[index]).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
return total;
}, '');
}
var badVariable = '<script>deleteEverything()</script>';
safeHTML`<div>${badVariable}</div>`; // Outputs: "<div><script>deleteEverything()</script></div>"
(By the way, don't actually write this kind of html escaping code and put it into production, as WebReflection points out below it is still vulnerable to XSS, but provides a simple illustration).
Replacing Handlebars (or Pug/Jade or Dust or whatever) with ES6 template strings
Okay, so we can use multiple lines, interpolate variables and escape html all natively within ES6. This should be enough to replace a library like Handlebars with ES6 template strings right?
Let's look at an Handlebars template example. This is just a basic example of how to use handlebars, we'll use this as a yard stick to compare how expressive we can get with template strings;
var Handlebars = require('handlebars');
Handlebars.registerHelper('capitalize', function (options) {
var string = String(options.fn(this));
return string[0].toUpperCase() + string.slice(1);
});
var page = Handlebars.compile(`
<h2>People</h2>
<ul>
{\{#each people}}
<li>
<span>{{capitalize this}}</span>
{\{#if isAdmin}}
<button>Delete {{capitalize this}}</button>
{\{/if}}
</li>
{\{/each}}
</ul>
`);
page({ isAdmin: true, people: ['Keith', 'Dave', 'Amy' ] });
First pass, ES6 template strings
Given the handlebars example, if we just get rid of Handlebars, and try to do that with just Template Literals plus a few wrapping functions, we end up with this:
function template(strings, ...interpolatedValues) {
return strings.reduce((total, current, index) => {
total += current;
if (interpolatedValues.hasOwnProperty(index)) {
total += String(interpolatedValues[index]).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
return total;
}, '');
}
function capitalize(string) {
return string[0].toUpperCase() + string.slice(1);
}
var page = ({ people, isAdmin }) => template`
<h2>People</h2>
<ul>
${people.map(person => `
<li>
<span>${capitalize(person)}</span>
${isAdmin ?
`<button>Delete ${capitalize(person)}</button>`
: ''}
</li>
`)}
</ul>
`;
page({ isAdmin: true, people: ['Keith', 'Dave', 'Amy' ] });
That's pretty clean! Well, I guess we're done here...
Not so fast. There's some very weird looking bits of that function. For starters, there are template tags within template tags, which means sprinkling lots of little annoying syntax everywhere. Secondly, we're using a ternary (${isAdmin}
) which forces us to include the "elsey" value (that weird : ''}
bit). Not only that, but Handlebars gives us loads of other helpers, like unless
, with
and lookup
. Handlebars also provides a mechanism for registering custom helpers, we could sure do with that! So what can we do to fix both of these problems?
Additional helpers for ES6 templates
Luckily, with ES6 template strings - we could easily make new helpers, they're just functions! Let's make a helpers object which can also be used to register new helpers:
function template(strings, ...interpolatedValues) {
return strings.reduce((total, current, index) => {
total += current;
if (interpolatedValues.hasOwnProperty(index)) {
total += String(interpolatedValues[index]).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
return total;
}, '');
}
var helpers = {
if: (condition, thenTemplate, elseTemplate = '') => {
return condition ? thenTemplate : elseTemplate;
},
unless: (condition, thenTemplate, elseTemplate) => {
return !condition ? thenTemplate : elseTemplate;
},
registerHelper(name, fn) => {
helpers[name] = fn;
}
};
helpers.registerHelper('capitalize', (string) => {
return string[0].toUpperCase() + string.slice(1).toUpperCase();
});
var page = ({ people, isAdmin }) => template`
<h2>People</h2>
<ul>
${people.map(person => `
<li>
<span>${helpers.capitalize(person)}</span>
${helpers.if(isAdmin,
`<button>Delete ${helpers.capitalize(person)}</button>`
)}
</li>
`)}
</ul>
`;
page({ isAdmin: true, people: ['Keith', 'Dave', 'Amy' ] });
Okay, this is a bit cleaner. There's less slightly syntax, and we have a nice way to add helpers which can be useful. There's still lots of back ticks everywhere though, is there a way we could get rid of those?
ES6 template strings with Symbols
So, as we're able to parse the template, and take all of its values - we can leverage the uniqueness of Symbols as a sentinel values. Sentinel values are just unique values, that we can detect in our code to trigger certain code paths. We can use sentinel values here, to trigger our template features, like the if conditional. Let's take a look at what we can do with Symbol sentinels values, by replacing the if
and adding a nice readable endIf
:
var startBlockSentinel = Symbol('blockSentinel');
var ignoreBlockSentinel = Symbol('ignoreBlockSentinel');
var endBlockSentinel = Symbol('endBlockSentinel');
var helpers = {
if: (condition, thenTemplate, elseTemplate = '') => {
return condition ? startBlockSentinel : ignoreBlockSentinel;
},
end: () => {
return endBlockSentinel;
},
unless: (condition, thenTemplate, elseTemplate) => {
return !condition ? startBlockSentinel : ignoreBlockSentinel;
},
registerHelper(name, fn) => {
helpers[name] = fn;
},
};
function template(strings, ...interpolatedValues) {
const blockNest = [];
return strings.reduce((total, current, index) => {
if (blockNest.includes(ignoreBlockSentinel)) { // If at any point we chose to ignore this block, skip this render pass
return total;
}
total += current;
if (interpolatedValues.hasOwnProperty(index)) {
var value = interpolatedValues[index];
if (value === startBlockSentinel || value === ignoreBlockSentinel) {
blockNest.push(value);
}
if (value === endBlockSentinel) {
blockNest.pop();
}
total += String(interpolatedValues[index]).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
return total;
}, '');
}
helpers.registerHelper('capitalize', (string) => {
return string[0].toUpperCase() + string.slice(1).toUpperCase();
});
var page = ({ people, isAdmin }) => template`
<h2>People</h2>
<ul>
${people.map(person) => `
<li>
<span>${helpers.capitalize(person)}</span>
${helpers.if(isAdmin)}
<button>Delete ${helpers.capitalize(person)}</button>
${helpers.end()}
</li>
`)}
</ul>
`;
page({ isAdmin: true, people: ['Keith', 'Dave', 'Amy' ] });
Hey, check that out! We've added a small amount of code to our template function, and we've managed to clean up our template from all the easy-to-miss syntax, we're no longer lost in a sea of parentheses and back ticks, instead we have some nice readable function calls.
Wait... is all this code really better?
It's a great question. We've come all this way, and it's questionable if this is any better than what Handlebars already provides us, in terms of an expressive syntax. We're not really making our code more concise, it's probably not much faster than Handlebars would be (when fully compiled), so it is questionable if this has all been worth it?
Ultimately, as you may have expected by now - I think the answer is no, not really. It was an interesting experiment, and showed some cool ES6 features, but Handlebars exists - has lots of great features and the developers have had plenty of time to work out all of the bugs and edge cases. What we have with our solution, is the beginnings of a huge commitment of time to get it to the level of refinement that Handlebars has.
Surely this hasn't been a total waste of time though? Have we learned anything from this? Is there anything we could take away from this? Well, I'm glad you asked, dear reader (you did ask right)?
Microtemplates are the best solution
The real answer, I think, is not that our solution was more expressive or not than Handlebars, it was that the fundamental concept of "monolithic templates" is broken in its expressiveness.
If you've spent time doing React, or any functional programming, then you might already be familiar with the concept of splitting out larger templates - with complex state and conditionals - into a series of smaller templates which can be managed much more easily. This, I believe, is where we can use Template Literals to our advantage. Our existing templates did too many complicated things! Let's break them down into much smaller templates:
var capitalize = (string) => {
return string[0].toUpperCase() + string.slice(1);
};
var adminDelete = ({ person }) => {
return `<button>Delete ${capitalize(person)}</button>`;
}
var personRecord = ({ person, deleteButton }) => {
return `
<li>
<span>${capitalize(person)}</span>
${deleteButton}
</li>
`;
}
var peopleList = ({ people, isAdmin }) => {
return `
<ul>
${people.map(person => personRecord({
person,
deleteButton: isAdmin ? adminDelete(person) : ''
})).join('')}
</ul>
`;
}
var page = ({ people, isAdmin }) => {
return `
<h2>People</h2>
${peopleList({ people, isAdmin })}
`;
}
By having these smaller functions, we have made a more manageable codebase because:
- Smaller functions are easier to test, they take a smaller set of data, and have a smaller range of outputs
- Smaller functions are composable! You can swap out parts easier, or re-arrange or refactor them with less work.
- Smaller functions are reusable! For example
personRecord
could easily be used where ever your app lists people, in a friends list, a block list, etc.
Conclusion
So Handlebars (or Dust or Jade or whatever templating language you are using or want to use) does a fine job. ES6 Template Literals probably won't be replacing those libaries any time soon, as there is little to gain from doing so. If you're using an existing templating library, you can feel confident that there's no new awesome thing that will shake up the landscape.
Having said that, maybe these template literals have given us a chance to step back and see that those templating languages aren't really fixing the problem in the right way? Maybe having small, composible template fragements is actually much more preferable, and maybe by breaking them down that small - we remove all of the need for these templating libraries?
What do you think about this? I'd love to hear your thoughts, musings and machinations around this topic. Feel free to send all hate mail to /dev/null
, otherwise, as always, speak up in the comments below or on Twitter, where I'm @keithamus.