DEV Community

Cover image for Template tags are just functions!
Antonio Villagra De La Cruz
Antonio Villagra De La Cruz

Posted on • Updated on • Originally published at antoniovdlc.me

Template tags are just functions!

A few years ago, ES6 introduced template literals, allowing among other things for multi-line strings, embedded expressions, and string interpolation.

That means that the following snippets of code could be written as follow:

console.log("This is the first line of a multi-line string.\n"
+ "And this is the second line!");

console.log(`This is the first line of a multi-line string.
And this is the second line!`);
Enter fullscreen mode Exit fullscreen mode
const a = 22;
const b = 20;

console.log("The answer to the ultimate question of life, the universe, and everything is " + (a + b) + "!");

console.log(`The answer to the ultimate question of life, the universe, and everything is ${a + b}!`);
Enter fullscreen mode Exit fullscreen mode

Template literals are already fairly useful with the above syntactic features, but there is more: template literals can be tagged!


Template tags are (mostly) functions that take in an array of strings as their first argument, and all expressions as the following arguments. Tags can then parse template literals as they see fit and return whichever value they see fit (not limited to strings).

const name1 = "Alice";
const name2 = "Bob";

function myTag (strings, fromName, toName) { 
  console.log(strings); // ["Template literal message from", " to ", " ..."]
  console.log(fromName); // "Alice"
  console.log(toName); // "Bob"

  ... 
}

console.log(myTag`Template literal message from ${name1} to ${name2} ...`);
Enter fullscreen mode Exit fullscreen mode

If no tags are provided to the template literal, the default tag simply concatenates the strings and expressions into a single string, for example:

function defaultTag(strings, ...expressions) {
  let str = "";
  for (let i = 0, l = strings.length; i < l; i++) {
    str += strings[i] + (expressions[i] != null ? expressions[i] : "");
  }
  return str;
}


const name1 = "Alice";
const name2 = "Bob";
const a = 22;
const b = 20;

console.log(defaultTag`Template literal message from ${name1} to ${name2}: 'The answer to the ultimate question of life, the universe, and everything is ${a + b}!'`);

// "Template literal message from Alice to Bob: 'The answer to the ultimate question of life, the universe, and everything is 42}!'"
Enter fullscreen mode Exit fullscreen mode

Note that there will always be more strings than expressions as empty strings will be added around expressions.


Now, we can probably build something a bit more interesting than just the default tag being applied to templates without tags!

Let's build a template tag that would allow us to format currency and numbers in certain ways. To better understand what we will build, let's look at an example:

const name = "Alice";
const number = 42;
const price = 20;

console.log(fmt`${name}:s has ${number}:n(1) oranges worth ${price}:c(USD)!`);
// "Alice has 42.0 oranges worth US$20.00!"
Enter fullscreen mode Exit fullscreen mode

This particular example is based on this article by Jack Hsu about building an internationalization library with template tags. Definitely worth a read!
https://jaysoo.ca/2014/03/20/i18n-with-es2015-template-literals/

Here, we specify that the value interpolated by ${name} should be treated as a string, the value interpolated by ${number} should be displayed as a number with one digit, and that the value interpolated by ${price} should be displayed with the USD currency, all that while respecting the user's locale.

First, we need to define a way to extract the formatting information from the string literals:

const fmtRegex = /^:([a-z])(\((.+)\))?/;

function extractFormatOption(literal) {
  let format = "s";
  let option = null;

  const match = fmtRegex.exec(literal);
  if (match) {
    if (Object.keys(formatters).includes(match[1])) {
      format = match[1];
    }

    option = match[3];
  }

  return { format, option };
}
Enter fullscreen mode Exit fullscreen mode

As an aside, every time I'm using regular expressions I am reminded of the following quote:

Some people, when confronted with a problem, think "I know, I'll use regular expressions." Now they have two problems. - Jamie Zawinski

But anyway, here we use a regular expression to match strings with our previously defined format, starting with : then a lower case letter, then an optional extra information in parenthesis.

The extractFormatOption() function simply helps us return the value of format and whatever option might have been passed as well. For example:

const { format, option } = extractFormatOption(`:c(USD)!`)
// format = "c"
// option = "USD"
Enter fullscreen mode Exit fullscreen mode

Next, we need a way to actually format those values. We will use an object whose fields correspond to the potential values of format.

const formatters = {
  c(str, currency) {
    return Number(str).toLocaleString(undefined, {
      style: "currency",
      currency,
    });
  },
  n(str, digits) {
    return Number(str).toLocaleString(undefined, {
      minimumFractionDigits: digits,
      maximumFractionDigits: digits,
    });
  },
  s(str) {
    return str != null ? str.toLocaleString() : "";
  },
};
Enter fullscreen mode Exit fullscreen mode

Finally, we update our defaultTag() function to support extra formatting:

function fmt(strings, ...expressions) {
  let str = "";
  for (let i = 0, l = strings.length; i < l; i++) {
    str += strings[i].replace(fmtRegex, "");

    const { format, option } = extractFormatOption(
      i + 1 < l ? strings[i + 1] : ""
    );

    str += formatters[format](expressions[i], option);
  }
  return str;
}
Enter fullscreen mode Exit fullscreen mode

Here we do a look-ahead and extract any format and option indications in the template literal (which default to "s"), then apply the corresponding formatter to the current expression we are interpolating.


As I found that exercise actually quite useful, I've published an npm package with more formatting options:

GitHub logo AntonioVdlC / fmt-tag

πŸ₯¨ - Format template literals

fmt-tag

version issues downloads license

Format template literals.

Installation

This package is distributed via npm:

npm install fmt-tag

Motivation

Template literals and template tags provide a unique API to build tools around strings What started as a fun blog post about template tags ended up being this full-fledged library that might hopefully be useful to someone!

Usage

You can use this library either as an ES module or a CommonJS package:

import fmt from "fmt-tag";
Enter fullscreen mode Exit fullscreen mode

- or -

const fmt = require("fmt-tag");
Enter fullscreen mode Exit fullscreen mode

Please note that this library uses extensively Intl, which is not supported on older browsers (https://caniuse.com/?search=Intl) or Node versions < 16.

You can tag any template literal and append formatting hints right after interpolations to apply specific formatting to that substitutive value.

const name = "Alice";
const money = 20;
console.log(fmt`${name} has ${money
…
Enter fullscreen mode Exit fullscreen mode

Latest comments (0)