November 20, 2018

ES6 Tips: Tagged Template Literals

Template literals are one of the first things I learned as I dove into ES6. They allow us to stop writing string concatenation like this:

const brand = 'La Croix';
const flavor = 'coconut';
const sentence = 'My favorite flavor of ' + brand + 'is ' + flavor + '.';

and start writing like this:

const sentence = `My favorite flavor of ${brand} is ${flavor}.`;

It might not seem like a big deal in small cases like this one, but the more variables you pepper into the string the more useful this is. No more hard-to-find syntax errors from one damn missing plus sign, or forgetting to leave a space! I wont go into the other benefits of template literals (like handling new lines with ease!) here, but if you want more information check out this article.

If you're using ES6 at all, you're likely already using template literals. But what about tagged template literals? I'll be honest, these threw my brain for a tiny loop at first. First things first: tagged template literals are functions. You define a function for handling a string, and pass in two arguments: strings and values. If you work in React with Styled Components or Apollo for GraphQL — you're already using tagged template literals!

A quick note if you want to code along as we go:

  1. Create an HTML file with a blank <script></script> tag. Open it in Chrome to view your work.
  2. Add some initial styles to the page in a <style></style> tag to visually show whats going on, for example:
.wrong {
  text-decoration: line-through;
  color: red;
}
.highlight {
  font-weight: bold;
  color: green;
}
  1. At the bottom of your script tag, add a document.write(sentence) as well as a console.log(sentence) to progressively build the demos.

Alternatively, you can just copy and paste each code block into the console, then ask for sentence and see what you get back.

// define the function
function faveDrink(strings, ...values){
    // magic happens in here, but for now we'll use debugger to see some info
    debugger;
}

const brand = 'La Croix';
const flavor = 'coconut';

// USE our tagged template literal!
const sentence = faveDrink`My favorite flavor of ${brand} is ${flavor}.`;

The function above takes in two parameters: strings and values. Note that we're using the ES6 rest operator for the second parameter ...values — this allows us to pass in as many values as we'd like! We could write faveDrink(strings, brand, flavor) but that will get messy when you accept more arguments, or don't know how many arguments there will be. Stick with the rest operator!

Both the strings and the values parameters are arrays. If we run the code above, your browser should pop open sources and show this in local scope:

Strings is an array made up of any non-variable material in your string. You can see each array item is a chunk of our original string, returned as it's own string. Note: if we didn't have a period at the end of our string, we would still have a third (empty) string in this array. More on this later. There's also the raw array, which we won't worry about for now — it's almost exactly the same, but if you were to include any escaped characters, it would leave them intact rather than read them as escaped.

Values is an array of our passed in strings! It's as simple as that. Note that there will always be one less array item in values than there is in strings — because even if we end our string with a template literal value, an empty string item will come back as the final item in the strings array. We need to remember this for when we re-assemble, and add logic accordingly!

How do we work with this, now that we know how to access our data? We write our function! There are endless possibilities for what you can do here, but I'll give you a few simple examples.

  1. Let's just return the string with some styling of the values we passed in!
function faveDrink(strings, ...values){
    let sentence = '';
  strings.forEach((string, i) => {
    sentence += `${string}${values[i] ? `<span class='highlight'>${values[i]}</span>` : ''}`;
  });
  return sentence; 
}

const brand = 'La Croix';
const flavor = 'coconut';

const sentence = faveDrink`My favorite flavor of ${brand} is ${flavor}.`;

All we're doing here is looping through each string, and inserting our values between them. You'll notice I'm checking for an empty value (to return nothing), since the [strings] array will always have a length of one more than [values]. The return value should be something like this:

My favorite flavor of <span class='highlight'>La Croix</span> is <span class='highlight'>coconut</span>.

When would this be helpful? Perhaps you have a list of users, each of which has an associated favorite brand and flavor of bubbly water (standard user info these days, right?). By defining this tagged template literal once, we can loop over users and format their data the same way.

  1. Manipulate the results based on what values get passed in. For ease of this example, I've taken away the option to choose your brand — users are only passing in flavors here.
function faveDrink(strings, ...values){
    const tastyFlavors = ['pamplemousse', 'coconut', 'peach-pear'];

    // make a new array of values, handling the manipulation where needed
    const tastyValues = values.map(value => {
      // if the input value is one of our predefined tasty flavors
      if (tastyFlavors.includes(value)) {
        // return it highlighted just like before
        return `<span class='highlight'>${value}</span>`;
      }
      // otherwise, choose a random tasty flavor to display instead
      let flavor = tastyFlavors[Math.floor(Math.random()*tastyFlavors.length)];
      return `<span class='wrong'>${value}</span> <span class='highlight'>${flavor}</span>`;
    });

    // join everything together from your new tastyValues array
    return strings.reduce((sentence, string, i) => {
      return sentence + string + (tastyValues[i] || '');
    }, '');
  }

const flavor = 'coconut';

const sentence = faveDrink`My favorite flavor of La Croix is ${flavor}.`;

In this example we predefine a few "acceptable" tasty flavors. If the user inputs one of those, the string is returned with the highlighting we did in example 1. If not, we cross out their answer, choose a random tasty flavor, and return it instead. Friends don't let friends drink passionfruit La Croix 😜. If you did put in an "unacceptable" flavor, you'd get a return like this:

My favorite flavor of La Croix is <span class='wrong'>passionfruit</span> <span class='highlight'>coconut</span>.
  1. Maybe you're so passionate about La Croix that you've written a blog post for each of your favorite flavors. When you return the string, you could link to your blog post based on what flavor the user inputs. Additionally, you could build a tooltip that gives a flavor-specific fact, or link to a sales page for that flavor.
const dataBase = {
    //  this would pull from your database, where only flavors with blog posts have been given facts
    coconut: 'Did you know coconut is the best flavor in the world?',
    pamplemousse: 'Pamplemousse is certifiably the second best flavor',
    //  ... the rest
  }

  function faveDrink(strings, ...values){
  //  make a new array of values
    const newValues = values.map(value => {
      // if the input value is recognized in our database
      if (dataBase[value]) {
        // return with a link out to the blog post AND a span to display tooltip fact
        return `<a href='blog/${value}' class='highlight linked'><span data-info='${dataBase[value]}'>${value}</span></a>`;
      }
      // otherwise, return it normally
      return `<span class='highlight'>${value}</span>`;
    });

    // join everything together from your new tastyValues array
    return strings.reduce((sentence, string, i) => {
      return sentence + string + (newValues[i] || '');
    }, '');
  }

  const brand = 'La Croix';
  const flavor = 'coconut';
  const sentence = faveDrink`My favorite flavor of ${brand} is ${flavor}.`;

This example builds on the previous one — we're checking a database to see if the input has associated data. If so, we return the value formatted with a link to the blog, and a tooltip (uncoded in the example, but the data-info in the span shows it). Otherwise, we return it highlighted as usual. We use the .reduce method to join everything together again. When you run this, you should see:

My favorite flavor of <span class='highlight'>La Croix</span> is <a href='blog/coconut' class='highlight linked'><span data-info='Did you know coconut is the best flavor in the world?'>coconut</span></a>.

These examples should get you going with tagged template literals, but I know they might still feel pretty abstract. What are some actual use cases?

  • On a page with technical terms, use our #3 approach to add a tooltip display for definitions, where they exist, or link to a more in-depth article.
  • Template an entire section dynamically (for example, a staff member biography card) by including full HTML in your tagged template literal. Your passed in strings will be staff member data from your database. Once everything comes back together, you'll have HTML chunks filled with data for each of your staff members.
  • Link to product pages anytime a writer mentions a product your company sells. The writer doesn't need to worry about this if your page automatically scrapes and formats words you've defined as items you sell!

How else do you use tagged template literals, or see them being useful? I'm still finding new ways to use them, and would love to hear from you!