Please support this book: buy it or donate

19. Using template literals and tagged templates



Before we dig into the two features template literal and tagged template, let’s first examine the multiple meanings of the term template.

19.1. Disambiguation: “template”

The following three things are significantly different, despite all having template in their names and despite all of them looking similar:

  • A web 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:
  1. <div class="entry">
  2. <h1>{{title}}</h1>
  3. <div class="body">
  4. {{body}}
  5. </div>
  6. </div>
  • A template literal is a string literal with more features. For example, interpolation. It is delimited by backticks:
  1. const num = 5;
  2. assert.equal(`Count: ${num}!`, 'Count: 5!');
  • A tagged template is a function followed by a template literal. It results in that function being called and the contents of the template literal being fed into it as parameters.
  1. const getArgs = (...args) => args;
  2. assert.deepEqual(
  3. getArgs`Count: ${5}!`,
  4. [['Count: ', '!'], 5] );

Note that getArgs() receives both the text of the literal and the data interpolated via ${}.

19.2. Template literals

Template literals have two main benefits, compared to normal string literals.

First, they support string interpolation: you can insert expressions if you put them inside ${ and }:

  1. const MAX = 100;
  2. function doSomeWork(x) {
  3. if (x > MAX) {
  4. throw new Error(`At most ${MAX} allowed: ${x}!`);
  5. }
  6. // ···
  7. }
  8. assert.throws(
  9. () => doSomeWork(101),
  10. {message: 'At most 100 allowed: 101!'});

Second, template literals can span multiple lines:

  1. const str = `this is
  2. a text with
  3. multiple lines`;

Template literals always produce strings.

19.3. Tagged templates

The expression in line A is a tagged template:

  1. const first = 'Lisa';
  2. const last = 'Simpson';
  3. const result = tagFunction`Hello ${first} ${last}!`; // A

The last line is equivalent to:

  1. const result = tagFunction(['Hello ', ' ', '!'], first, last);

The parameters of tagFunction are:

  • Template strings (first parameter): an Array with the text fragments surrounding the interpolations (${···}).
    • In the example: ['Hello ', ' ', '!']
  • Substitutions (remaining parameters): the interpolated values.
    • In the example: 'Lisa' and 'Simpson'
      The static (fixed) parts of the literal (the template strings) are separated from the dynamic parts (the substitutions).

tagFunction can return arbitrary values and gets two views of the template strings as input (only the cooked view is shown in the previous example):

  • A cooked view where, e.g.:
    • \t becomes a tab
    • \ becomes a single backslash
  • A raw view where, e.g.:
    • \t becomes a slash followed by a t
    • \ becomes two backslashes
      The raw view enables raw string literals via String.raw (described later) and similar applications.

Tagged templates are great for supporting small embedded languages (so-called domain-specific languages). We’ll continue with a few examples.

19.3.1. Tag function library: lit-html

lit-html is a templating library that is based on tagged templates and used by the frontend framework Polymer:

  1. import {html, render} from 'lit-html';
  2. const template = (items) => html`
  3. <ul>
  4. ${
  5. repeat(items,
  6. (item) => item.id,
  7. (item, index) => html`<li>${index}. ${item.name}</li>`
  8. )
  9. }
  10. </ul>
  11. `;

repeat() is a custom function for looping. Its 2nd parameter produces unique keys for the values returned by the 3rd parameter. Note the nested tagged template used by that parameter.

19.3.2. Tag function library: re-template-tag

re-template-tag is a simple library for composing regular expressions. Templates tagged with re produce regular expressions. The main benefit is that you can interpolate regular expressions and plain text via ${} (see RE_DATE):

  1. import {re} from 're-template-tag';
  2. const RE_YEAR = re`(?<year>[0-9]{4})`;
  3. const RE_MONTH = re`(?<month>[0-9]{2})`;
  4. const RE_DAY = re`(?<day>[0-9]{2})`;
  5. const RE_DATE = re`/${RE_YEAR}-${RE_MONTH}-${RE_DAY}/u`;
  6. const match = RE_DATE.exec('2017-01-27');
  7. assert.equal(match.groups.year, '2017');

19.3.3. Tag function library: graphql-tag

The library graphql-tag lets you create GraphQL queries via tagged templates:

  1. import gql from 'graphql-tag';
  2. const query = gql`
  3. {
  4. user(id: 5) {
  5. firstName
  6. lastName
  7. }
  8. }
  9. `;

Additionally, there are plugins for pre-compiling such queries in Babel, TypeScript, etc.

19.4. Raw string literals

Raw string literals are implemented via the tag function String.raw. They are a string literal where backslashes don’t do anything special (such as escaping characters etc.):

  1. assert.equal(String.raw`\back`, '\\back');

One example where that helps is strings with regular expressions:

  1. const regex1 = /^\./;
  2. const regex2 = new RegExp('^\\.');
  3. const regex3 = new RegExp(String.raw`^\.`);

All three regular expressions are equivalent. You can see that with a string literal, you have to write the backslash twice to escape it for that literal. With a raw string literal, you don’t have to do that.

Another example where raw string literal are useful is Windows paths:

  1. const WIN_PATH = String.raw`C:\foo\bar`;
  2. assert.equal(WIN_PATH, 'C:\\foo\\bar');

19.5. (Advanced)

All remaining sections are advanced

19.6. Multi-line template literals and indentation

If you put multi-line text in template literals, two goals are in conflict: On one hand, the text should be indented to fit inside the source code. On the other hand, its lines should start in the leftmost column.

For example:

  1. function div(text) {
  2. return `
  3. <div>
  4. ${text}
  5. </div>
  6. `;
  7. }
  8. console.log('Output:');
  9. console.log(div('Hello!')
  10. // Replace spaces with mid-dots:
  11. .replace(/ /g, '·')
  12. // Replace \n with #\n:
  13. .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.

  1. Output:
  2. #
  3. ····<div>#
  4. ······Hello!#
  5. ····</div>#
  6. ··

There are two ways to fix this: via a tagged template or by trimming the result of the template literal.

19.6.1. Fix: template tag for dedenting

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 cuts off the indents 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:

  1. import dedent from 'dedent';
  2. function divDedented(text) {
  3. return dedent`
  4. <div>
  5. ${text}
  6. </div>
  7. `;
  8. }
  9. console.log('Output:');
  10. console.log(divDedented('Hello!'));

This time, the output is not indented:

  1. Output:
  2. <div>
  3. Hello!
  4. </div>

19.6.2. Fix: .trim()

The second fix is quicker, but also dirtier:

  1. function divDedented(text) {
  2. return `
  3. <div>
  4. ${text}
  5. </div>
  6. `.trim();
  7. }
  8. console.log('Output:');
  9. 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 not needing a custom tag function. The downside is that it looks ugly.

The output looks like it did with dedent (however, there is no line break at the end):

  1. Output:
  2. <div>
  3. Hello!
  4. </div>

19.7. Simple templating via template literals

While template literals look like web templates, it is not immediately obvious how to use them for (web) templating: A web 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:

  1. const tmpl = (data) => `Hello ${data.name}!`;
  2. assert.equal(tmpl({name: 'Jane'}), 'Hello Jane!');

19.7.1. A more complex example

As a more complex example, we’d like to take an Array of addresses and produce an HTML table. This is the Array:

  1. const addresses = [
  2. { first: '<Jane>', last: 'Bond' },
  3. { first: 'Lars', last: '<Croft>' },
  4. ];

The function tmpl() that produces the HTML table looks as follows.

  1. const tmpl = (addrs) => `
  2. <table>
  3. ${addrs.map(
  4. (addr) => `
  5. <tr>
  6. <td>${escapeHtml(addr.first)}</td>
  7. <td>${escapeHtml(addr.last)}</td>
  8. </tr>
  9. `.trim()
  10. ).join('')}
  11. </table>
  12. `.trim();

tmpl() takes the following steps:

  • The text inside the <table> is produced via a nested templating function for single addresses (line 4). Note how it uses the string method .trim() at the end, to remove unnecessary whitespace.
  • The nested templating function is applied to each element of the Array addrs via the Array method .map() (line 3).
  • The resulting Array (of strings) is converted into a string via the Array method .join() (line 10).
  • The helper function escapeHtml() is used to escape special HTML characters (line 6 and line 7). Its implementation is shown in the next section.
    This is how to call tmpl() with the addresses and log the result:
  1. console.log(tmpl(addresses));

The output is:

  1. <table>
  2. <tr>
  3. <td>&lt;Jane&gt;</td>
  4. <td>Bond</td>
  5. </tr><tr>
  6. <td>Lars</td>
  7. <td>&lt;Croft&gt;</td>
  8. </tr>
  9. </table>

19.7.2. Simple HTML-escaping

  1. function escapeHtml(str) {
  2. return str
  3. .replace(/&/g, '&amp;') // first!
  4. .replace(/>/g, '&gt;')
  5. .replace(/</g, '&lt;')
  6. .replace(/"/g, '&quot;')
  7. .replace(/'/g, '&#39;')
  8. .replace(/`/g, '&#96;')
  9. ;
  10. }

19.8. Further reading