Write Your Own JavaScript Template Tag

March 13, 2020 Programming, JavaScript,

Illustration: Raymond Sohn

Introduction

Template strings have been a game-changing addition to the JavaScript language but that’s not what I want to talk about. I’d like to draw your attention to the uncommon and perhaps misunderstood tagged template string.

Tagged template strings are potent, and could potentially be used for: embedding domain specific languages like Markdown, embedding programming languages like ClojureScript, metaprogramming with Babel to generate code and inspect abstract syntax trees, string manipulation and more.

Normally, a template string performs string interpolation [1] . Whereas, a tagged template string allows you to hijack the entire process and return anything you want. Take the example below, where I’m using the GraphQL tag to define a query. It doesn’t return a string at all. Instead, it transforms the template string into a GraphQL syntax tree.

1let query = gql`{
2  users {
3    name
4  }
5}`;
6
7console.log(query);

Output

{
  kind: 'Document',
  definitions: [
    {
      kind: 'OperationDefinition',
      operation: 'query',
      selectionSet: [...],
      ...
    }
  ]
}

Tagged template strings are powerful; but don’t be intimidated. They’re easy to understand because they’re built out of 2 things that you already know about: functions and template strings. To put it all together, a tagged template string is a combination of:

  1. a tag (which is just a function).
  2. a template string.

Understanding the Arguments

The first step towards writing a tag is to understand how arguments are passed to a tag. Before a tag receives a template string, JavaScript splits the template string apart into strings and values. The strings are the substrings that make up the template string. And the values are the expressions that were embedded into the template string.

1// A template tag that prints its own arguments
2function argsTag(strings, ...values) {
3  console.log({ strings, values });
4}
5
6argsTag`Hello ${'World'}!`

Output

{
  strings: ['Hello', '!'],
  values: ['World']
}

Building a Template String

The next step is to figure out how to build a template string given those arguments. A straight-forward way to combine the arguments is with a for loop. But if you’re functionally-curious you could zip, flatten and concatenate.

 1// A template tag that builds a template string
 2function buildTag(strings, ...values) {
 3  let result = '';
 4  // Append each string to the result.
 5  strings.forEach((string, index) => {
 6    result += string;
 7    // If a value exists then append it to the result.
 8    if (index < values.length) {
 9      result += values[index];
10    }
11  });
12  return result;
13}
14
15buildTag`Hello ${'World'}!`

Output

'Hello World!'

Transforming the Result

The final step is to transform the result in anyway you want. For my tag, I decided to port over Ruby on Rails’ squish method which collapses consecutive spaces into single spaces.

 1// A template tag that squishes whitespace
 2function squishTag(strings, ...values) {
 3  let result = '';
 4  strings.forEach((string, index) => {
 5    result += string;
 6    if (index < values.length) {
 7      result += values[index];
 8    }
 9  });
10  return result.trim().replace(/\s+/g, ' ');
11}
12
13squishTag`
14  Hello
15  ${'World'}
16  !
17`

Output

'Hello World !'
Thanks to Michael Geraci and MarĂ­n Alcaraz for editing.
If you liked this post, you might also like Keyboard Events TL;DR or JavaScript Arrow Function Gotchas.
Raymond Sohn. Powered by Hugo.