Before we dig into the two features template literal and tagged template, let’s first examine the multiple meanings of the term template.
The following three things are significantly different despite all having template in their names and despite all of them looking similar:
A text template is a function from data to text. It is frequently used in web development and often defined via text files. For example, the following text defines a template for the library Handlebars:
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>
This template has two blanks to be filled in: title
and body
. It is used like this:
// First step: retrieve the template text, e.g. from a text file.
const tmplFunc = Handlebars.compile(TMPL_TEXT); // compile string
const data = {title: 'My page', body: 'Welcome to my page!'};
const html = tmplFunc(data);
A template literal is similar to a string literal, but has additional features – for example, interpolation. It is delimited by backticks:
const num = 5;
assert.equal(`Count: ${num}!`, 'Count: 5!');
Syntactically, a tagged template is a template literal that follows a function (or rather, an expression that evaluates to a function). That leads to the function being called. Its arguments are derived from the contents of the template literal.
const getArgs = (...args) => args;
assert.deepEqual(
getArgs`Count: ${5}!`,
[['Count: ', '!'], 5] );
Note that getArgs()
receives both the text of the literal and the data interpolated via ${}
.
A template literal has two new features compared to a normal string literal.
First, it supports string interpolation: if we put a dynamically computed value inside a ${}
, it is converted to a string and inserted into the string returned by the literal.
const MAX = 100;
function doSomeWork(x) {
if (x > MAX) {
throw new Error(`At most ${MAX} allowed: ${x}!`);
}
// ···
}
assert.throws(
() => doSomeWork(101),
{message: 'At most 100 allowed: 101!'});
Second, template literals can span multiple lines:
const str = `this is
a text with
multiple lines`;
Template literals always produce strings.
The expression in line A is a tagged template. It is equivalent to invoking tagFunc()
with the arguments listed in the Array in line B.
function tagFunc(...args) {
return args;
}
const setting = 'dark mode';
const value = true;
assert.deepEqual(
tagFunc`Setting ${setting} is ${value}!`, // (A)
[['Setting ', ' is ', '!'], 'dark mode', true] // (B)
);
The function tagFunc
before the first backtick is called a tag function. Its arguments are:
Template strings (first argument): an Array with the text fragments surrounding the interpolations ${}
.
['Setting ', ' is ', '!']
Substitutions (remaining arguments): the interpolated values.
'dark mode'
and true
The static (fixed) parts of the literal (the template strings) are kept separate from the dynamic parts (the substitutions).
A tag function can return arbitrary values.
So far, we have only seen the cooked interpretation of template strings. But tag functions actually get two interpretations:
A cooked interpretation where backslashes have special meaning. For example, \t
produces a tab character. This interpretation of the template strings is stored as an Array in the first argument.
A raw interpretation where backslashes do not have special meaning. For example, \t
produces two characters – a backslash and a t
. This interpretation of the template strings is stored in property .raw
of the first argument (an Array).
The raw interpretation enables raw string literals via String.raw
(described later) and similar applications.
The following tag function cookedRaw
uses both interpretations:
function cookedRaw(templateStrings, ...substitutions) {
return {
cooked: Array.from(templateStrings), // copy only Array elements
raw: templateStrings.raw,
substitutions,
};
}
assert.deepEqual(
cookedRaw`\tab${'subst'}\newline\\`,
{
cooked: ['\tab', '\newline\\'],
raw: ['\\tab', '\\newline\\\\'],
substitutions: ['subst'],
});
We can also use Unicode code point escapes (\u{1F642}
), Unicode code unit escapes (\u03A9
), and ASCII escapes (\x52
) in tagged templates:
assert.deepEqual(
cookedRaw`\u{54}\u0065\x78t`,
{
cooked: ['Text'],
raw: ['\\u{54}\\u0065\\x78t'],
substitutions: [],
});
If the syntax of one of these escapes isn’t correct, the corresponding cooked template string is undefined
, while the raw version is still verbatim:
assert.deepEqual(
cookedRaw`\uu\xx ${1} after`,
{
cooked: [undefined, ' after'],
raw: ['\\uu\\xx ', ' after'],
substitutions: [1],
});
Incorrect escapes produce syntax errors in template literals and string literals. Before ES2018, they even produced errors in tagged templates. Why was that changed? We can now use tagged templates for text that was previously illegal – for example:
windowsPath`C:\uuu\xxx\111`
latex`\unicode`
Tagged templates are great for supporting small embedded languages (so-called domain-specific languages). We’ll continue with a few examples.
Lit is a library for building web components that uses tagged templates for HTML templating:
@customElement('my-element')
class MyElement extends LitElement {
// ···
render() {
return html`
<ul>
${repeat(
this.items,
(item) => item.id,
(item, index) => html`<li>${index}: ${item.name}</li>`
)}
</ul>
`;
}
}
repeat()
is a custom function for looping. Its second parameter produces unique keys for the values returned by the third parameter. Note the nested tagged template used by that parameter.
The library “regex” by Steven Levithan provides template tags that help with creating regular expressions and enable advanced features. The following example demonstrates how it works:
import {regex, pattern} from 'regex';
const RE_YEAR = pattern`(?<year>[0-9]{4})`;
const RE_MONTH = pattern`(?<month>[0-9]{2})`;
const RE_DAY = pattern`(?<day>[0-9]{2})`;
const RE_DATE = regex('g')`
${RE_YEAR} # 4 digits
-
${RE_MONTH} # 2 digits
-
${RE_DAY} # 2 digits
`;
const match = RE_DATE.exec('2017-01-27');
assert.equal(match.groups.year, '2017');
The following flags are switched on by default:
/v
/x
(emulated) enables insignificant whitespace and line comments via #
.
/n
(emulated) enables named capture only mode, which prevents the grouping metacharacters (···)
from capturing.
The library graphql-tag lets us create GraphQL queries via tagged templates:
import gql from 'graphql-tag';
const query = gql`
{
user(id: 5) {
firstName
lastName
}
}
`;
Additionally, there are plugins for pre-compiling such queries in Babel, TypeScript, etc.
Raw string literals are implemented via the tag function String.raw
. They are string literals where backslashes don’t do anything special (such as escaping characters, etc.):
assert.equal(String.raw`\back`, '\\back');
This helps whenever data contains backslashes – for example, strings with regular expressions:
const regex1 = /^\./;
const regex2 = new RegExp('^\\.');
const regex3 = new RegExp(String.raw`^\.`);
All three regular expressions are equivalent. With a normal string literal, we have to write the backslash twice, to escape it for that literal. With a raw string literal, we don’t have to do that.
Raw string literals are also useful for specifying Windows filename paths:
const WIN_PATH = String.raw`C:\foo\bar`;
assert.equal(WIN_PATH, 'C:\\foo\\bar');
All remaining sections are advanced
If we put multiline text in template literals, two goals are in conflict: On one hand, the template literal should be indented to fit inside the source code. On the other hand, the lines of its content should start in the leftmost column.
For example:
function div(text) {
return `
<div>
${text}
</div>
`;
}
console.log('Output:');
console.log(
div('Hello!')
// Replace spaces with mid-dots:
.replace(/ /g, '·')
// Replace \n with #\n:
.replace(/\n/g, '#\n')
);
Due to the indentation, the template literal fits well into the source code. Alas, the output is also indented. And we don’t want the return at the beginning and the return plus two spaces at the end.
Output:
#
····<div>#
······Hello!#
····</div>#
··
There are two ways to fix this: via a tagged template or by trimming the result of the template literal.
The first fix is to use a custom template tag that removes the unwanted whitespace. It uses the first line after the initial line break to determine in which column the text starts and shortens the indentation everywhere. It also removes the line break at the very beginning and the indentation at the very end. One such template tag is dedent
by Desmond Brand:
import dedent from 'dedent';
function divDedented(text) {
return dedent`
<div>
${text}
</div>
`.replace(/\n/g, '#\n');
}
console.log('Output:');
console.log(divDedented('Hello!'));
This time, the output is not indented:
Output:
<div>#
Hello!#
</div>
.trim()
The second fix is quicker, but also dirtier:
function divDedented(text) {
return `
<div>
${text}
</div>
`.trim().replace(/\n/g, '#\n');
}
console.log('Output:');
console.log(divDedented('Hello!'));
The string method .trim()
removes the superfluous whitespace at the beginning and at the end, but the content itself must start in the leftmost column. The advantage of this solution is that we don’t need a custom tag function. The downside is that it looks ugly.
The output is the same as with dedent
:
Output:
<div>#
Hello!#
</div>
While template literals look like text templates, it is not immediately obvious how to use them for (text) templating: A text template gets its data from an object, while a template literal gets its data from variables. The solution is to use a template literal in the body of a function whose parameter receives the templating data – for example:
const tmpl = (data) => `Hello ${data.name}!`;
assert.equal(tmpl({name: 'Jane'}), 'Hello Jane!');
As a more complex example, we’d like to take an Array of addresses and produce an HTML table. This is the Array:
const addresses = [
{ first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
];
The function tmpl()
that produces the HTML table looks as follows:
const tmpl = (addrs) => `
<table>
${addrs.map(
(addr) => `
<tr>
<td>${escapeHtml(addr.first)}</td>
<td>${escapeHtml(addr.last)}</td>
</tr>
`.trim()
).join('')}
</table>
`.trim();
This code contains two templating functions:
addrs
, an Array with addresses, and returns a string with a table.
addr
, an object containing an address, and returns a string with a table row. Note the .trim()
at the end, which removes unnecessary whitespace.
The first templating function produces its result by wrapping a table element around an Array that it joins into a string (line 10). That Array is produced by mapping the second templating function to each element of addrs
(line 3). It therefore contains strings with table rows.
The helper function escapeHtml()
is used to escape special HTML characters (line 6 and line 7). Its implementation is shown in the next subsection.
Let us call tmpl()
with the addresses and log the result:
console.log(tmpl(addresses));
The output is:
<table>
<tr>
<td><Jane></td>
<td>Bond</td>
</tr><tr>
<td>Lars</td>
<td><Croft></td>
</tr>
</table>
The following function escapes plain text so that it is displayed verbatim in HTML:
function escapeHtml(str) {
return str
.replace(/&/g, '&') // first!
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/`/g, '`')
;
}
assert.equal(
escapeHtml('Rock & Roll'), 'Rock & Roll');
assert.equal(
escapeHtml('<blank>'), '<blank>');
Exercise: HTML templating
Exercise with bonus challenge: exercises/template-literals/templating_test.mjs