JavaScript: Tagged Template Literals

Using a general template literal we can construct a string simply and effectively. On top of that, we have Tagged Template Literal, which provides additional functionality.

Using tagged template literals we can parse a template literal string and then we can perform any operation on the parts of the template literal if required.

For tagged template literal we have to define a function, that we can use to parse and manipulate the template string. Defining a template tag function is simple, we just need to take care of the parameters. There is no special rule for defining the function.

Implementation Steps

The template tag function gives us 2 sets of data-

  1. First (single) argument: Parts of the template string as an array(excluding the values)
  2. Rest of the arguments: All the values set in the template string

As these data are provided in parts, so we can make any required changes to the specific part of the string.

For the demo of the implementation, let’s consider a template literal that will take a description of something(like a car, bike, etc.) and highlight the provided values.

Let’s check the implementation and usage of tagged template literals, step by step-

Step #1: Declare a Template Tag Function

Template tag function declaration is just like a normal function the difference is in the arguments. There is no fixed rule for naming the arguments.

  • Declare a function, and define a few arguments. Here we have a function named “carDesc”, and the arguments “templateStr”, “value1”, “value2”, “value3”.
  • For now, just log the arguments inside the function, so that we can check what the arguments represent.
  • Later, we are using the function “carDesc” and pass a string that has a car description.
  • We have some values in the description string.
  • Wrap the string with backtick(`) and parentheses are not required here.
// Define a function for the car description tag
function carDesc(templateStr, value1, value2, value3) {
    console.log(templateStr)
    console.log(value1)
    console.log(value2)
    console.log(value3)
}

let carManufacturer = "Ford";
let carModel = 'Mustang GT';
let carKeyFeature = 'V8 engine';

// Call the tag function
carDesc`Experience automotive excellence with the ${carModel}. 
The ${carKeyFeature} makes this car unique. 
This car is manufactured by ${carManufacturer} company.`

Output:

We get the following output, here are the key points to notice in the output-

  • The first argument “templateStr” has the value of the parts of the provided description string as an array. All the value parts are not included in that array, and the string is split in the place of those value parts, for making each element of the array.
  • Other arguments have a single value for each of the values that were provided in the string. In this case, the car name, model, manufacturer, etc.
Array(4)[
    0: "Experience automotive excellence with the "
    1: ". \nThe "
    2: " makes this car unique. \nThis car is manufactured by "
    3: " company
]

Mustang GT
V8 engine
Ford

Notes

  • Use the tagged template function without parentheses.

Step #2: Use Rest Operator

Use rest parameter for the second param, as the number of arguments(values used in the string) is not fixed.

  • In the “carDesc” keep the first param “templateStr”, as it is giving the array of split strings (except the values).
  • Instead of the other parameters use a rest parameter(…values), so that it can accept any number of values as an array. We will get all the values set in the passed string, in this array(values).
// Define a function for the car description tag
function carDesc(templateStr, ...values) {
    console.log(templateStr);
    console.log(values);
}

let carManufacturer = "Ford";
let carModel = 'Mustang GT';
let carKeyFeature = 'V8 engine';

// Call the tag function
carDesc`Experience automotive excellence with the ${carModel}. 
The ${carKeyFeature} makes this car unique. 
This car is manufactured by ${carManufacturer} company.`

Output:

  • We will get the car name, feature, and manufacturer in the “values” array, as the “values” is a rest parameter now.
Array(4)[
    0: "Experience automotive excellence with the "
    1: ". \nThe "
    2: " makes this car unique. \nThis car is manufactured by "
    3: " company."
]

Array(3)[
    0: "Mustang GT"
    1: "V8 engine"
    2: "Ford"
]

Step #3: Manipulate Data and Return

We can perform any manipulation of the string parts or values, inside the tag function. We have to return the result at the end of the function.

  • As a part of manipulation, we are adding a “<strong>” tag to wrap the values, to show the values as bold.
  • Here “reduce” function is used to combine the parts of the arrays and generate the final string.
  • At the end of the tag function, we are returning the resulting string.
// Define a function for the car description tag
function carDesc(templateStr, ...values) {
    console.log(templateStr);
    console.log(values);

    // Combine all data from the parts of the template literal provided as array
    // Add bold tag to highlight the values provided in the template string
    return templateStr.reduce((result, current, currentIndex) => {
        const formattedValue = values[currentIndex] ? `<strong>${values[currentIndex]}</strong>` : '';
                                                                 
        return result + current + formattedValue;
    }, '');
}

let carManufacturer = "Ford";
let carModel = 'Mustang GT';
let carKeyFeature = 'V8 engine';

// Call the tag function
const formattedDesc = carDesc`Experience automotive excellence with the ${carModel}. 
The ${carKeyFeature} makes this car unique. 
This car is manufactured by ${carManufacturer} company.`;

console.log(formattedDesc);

Output:

  • In the final result, we have the full string, just the values are highlighted with “<strong>” tags.
Experience automotive excellence with the <strong>Mustang GT</strong>. 
The <strong>V8 engine</strong> makes this car unique. 
This car is manufactured by <strong>Ford</strong> company.

Notes

  • We can perform any manipulation, there is no rule or conversion restricting us. It can be some small changes, or can be big changes that completely change the provided output.
  • The tag template function function may or may not return value. It is not mandatory to return value from the tag function.
  • In the case of returning a value, the tag template function, it is not mandatory to return a string. The function can return anything, like an array, or a function.
  • As the tag template function can return a function, so we can perform recursive calls like the below-
    function tagRec(strings, …values) {
    console.log(strings, values);
    return tagRec;
    }

    tagRec`foo``bar`;


    This will output-
    Array(1) [0: foo] Array(0) []
    Array(1) [0: bar] Array(0) []

Examples

Take a look at the following examples of Tagged Template Literals implementation and usage-

Example #1: Link @user and URLs

In this example-

  • We are implementing a tag function for processing a log message.
  • We are checking if the values have a ‘@’ sign in the beginning. If ‘@’ sign is there, then it means it is mentioning a user account. So we are adding a ‘<a>’ tag with the user account link.
  • We are also looking for email address. If there is any email address, then we are adding a link with “mailto:”.
const siteUrl = 'https://bigboxcode.com';
const profileUrlPrefix = 'user';
const hashtagUrlPrefix = 'tag';
const emailRegex = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/;

function logMessage(message, ...values) {
    return message.reduce((result, current, currentIndex) => {
        let formattedValue = '';
        
        if(values[currentIndex]) {
            let link;

            // Check if the value starts with a '@' sign
            // It is a user name in that case
            if(values[currentIndex].startsWith('@')) {
                link = `${siteUrl}/${profileUrlPrefix}/${values[currentIndex]}`;
            }

            // Check if the value matches email format
            if (emailRegex.test(values[currentIndex])) {
                link = `mailto:${values[currentIndex]}`;
            }

            // Make the value part bold in the log for all values
            formattedValue = `<strong>${values[currentIndex]}</strong>`;

            // If link is available then add tag for link
            if (link) {
                formattedValue = `<a href="${link}">${formattedValue}</a>`;
            }
        }
                                                                 
        return result + current + formattedValue;
    }, '');
}

// Let's generate a log for a content creation
const logUser1 = "@robb";
const contentTitle1 = "Mastering JavaScript Array";


const log1 = logMessage`User ${logUser1} has posted a new content - ${contentTitle1}`;

console.log(log1);


// Generate a log for user email change
const logUser2 = "@lottie";
const email = "lottie@bigboxcode.com";

const log2 = logMessage`${logUser2} updated his email address to ${email}`;

console.log(log2);

Output:

User <a href="https://bigboxcode.com/user/@robb"><strong>@robb</strong></a> has posted a new content - <strong>Mastering JavaScript Array</strong>

<a href="https://bigboxcode.com/user/@lottie"><strong>@lottie</strong></a> updated his email address to <a href="mailto:lottie@bigboxcode.com"><strong>lottie@bigboxcode.com</strong></a>

Example #2: Add comma as thousand separator

In this example-

  • We are manipulating the part of the string. We are adding comma separation for large number readability.
// Define template tag function for formatting message
function formatTemplate(messages, ...values) {
    let result = '';

    for (let i = 0; i < messages.length; i++) {
        result += messages[i];
        let currentValue = values[i];

        if (i < values.length && typeof currentValue === 'number') {
            currentValue = currentValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
        }

        result += currentValue;
    }


    return result;
}

// Define values
const name = 'Kadin Cremin';
const totalAmount = 1234567890;
const feeAmount = totalAmount * 2 / 100;
const paymentDate = new Date(Date.now() + 48 * 3600 * 1000);

// Get formatted message
const finalMessage = formatTemplate`
  <div>
    <p>Dear ${name},</p>
    <p>
        We received a deposit of amout ${totalAmount} USD.
        We will process the deposit by ${paymentDate.toLocaleDateString()}.
        Amount of ${feeAmount} USD will be deducted as a processing fee.
        So ${totalAmount -feeAmount} USD will be added to your account, after processing.
    </p>
    <p>Sincerely<p>
    <p>BigBox Bank</p>
  </div>
`;

console.log(finalMessage);

Output:

<div>
    <p>Dear Kadin Cremin,</p>
    <p>
        We received a deposit of amout 1,234,567,890 USD.
        We will process the deposit by 12/22/2023.
        Amount of 24,691,357.8 USD will be deducted as a processing fee.
        So 1,209,876,532.2 USD will be added to your account, after processing.
    </p>
    <p>Sincerely<p>
    <p>BigBox Bank</p>
  </div>

Example #3: Translate strings

In this example-

  • We are implementing string translation.
  • We have a selected language defined, and some translation strings for different languages.
  • If translation is available for the part of the strings, then we are replacing the original string with the translation.
const selectedLang = "fr";

// This translation data can come from any source
const translations = {
  login: {
    jp: "ログイン",
    fr: "se connecter",
    hi: "लॉग इन करें",
  },
  register: {
    jp: "登録する",
    fr: "registre",
    hi: "पंजीकरण करवाना",
  },
  logout: {
    jp: "ログアウト",
    fr: "Se déconnecter",
    hi: "लॉग आउट",
  },
  profile: {
    jp: "プロフィール",
    fr: "profil",
    hi: "प्रोफ़ाइल",
  },
  "change your profile settings": {
    jp: "プロフィール設定を変更する",
    fr: "modifier les paramètres de votre profil",
    hi: "अपनी प्रोफ़ाइल सेटिंग बदलें",
  },
  hello: {
    jp: "こんにちは",
    fr: "Bonjour",
    hi: "स्वागत",
  },
};

// Tag function for translation
function translate(rootString, ...args) {
  return rootString.reduce((result, current, currentIndex) => {
    const trimCurrent = current.trim().toLowerCase();
    const currentTranslation =
      translations?.[trimCurrent]?.[selectedLang] ?? trimCurrent;

    return (
      result +
      current.replace(trimCurrent, currentTranslation) +
      (args[currentIndex] ?? "")
    );
  }, "");
}

// Translation example 1
const name = "Axel Johns";

const greetingMessage = translate`Hello ${name}`;

console.log(greetingMessage);

// Translation example 2
const settingChangeMessage = translate`change your profile settings`;

console.log(settingChangeMessage);

// Translation example 3
const unknownString = translate`This is an unknown string, translation does not exist.`;

console.log(unknownString);

Output:

Hello Axel Johns
modifier les paramètres de votre profil
This is an unknown string, translation does not exist.

Example #4: SQL query value sanitization

In this example-

  • We are implementing a string tag template function for sanitizing provided values from an SQL query.
  • Declared a utility function for sanitization. This function is not part of the tag template implementation.
  • Defined a tag template function named ‘sql’. We are sanitizing the values in the function and combining the full string. 
// Utility function for input sanitization
function sanitize(value) {
  value = value.toString();

  // Remove semicolon(;)
  value = value.replace(";", "");

  // Replace single quote with double single quote
  value = value.replace(/'/g, "''");

  // Perform other sanization steps

  // return sanitized value
  return `'${value}'`;
}

// Tag function
function sql(queryStrings, ...queryValues) {
  let finalQuery = "";

  for (let i = 0; i < queryStrings.length; i++) {
    finalQuery += queryStrings[i];

    if (i < queryValues.length) {
      finalQuery += sanitize(queryValues[i]);
    }
  }

  // We are returing the query here
  // But if we want then we can process/execute query here
  // and return the result

  return finalQuery;
}

// Check with an insert query
// First name has a single quote in it
const firstName = "O'Connor";
const lastName = "Johnson";
const address = "11950 Dublin Canyon Rd, Pleasanton, California, 94588";
const phone = "(925) 847-6000";

const insertQuery = sql`
    INSERT INTO customer (first_name, last_name, address, phone, status)
    VALUES (${firstName}, ${lastName}, ${address}, ${phone}, ${"open"})
  `;

console.log(insertQuery);

// Check with a query that has SQL inject attpemt
const customerId = "abc'; DROP TABLE customer; --";

const selectQuery = sql`SELECT * FROM customer WHERE id = ${customerId}`;

console.log(selectQuery);

Output:

Here are the sanitized queries, which are saved to use-

INSERT INTO customer (first_name, last_name, address, phone, status)
    VALUES ('O''Connor', 'Johnson', '11950 Dublin Canyon Rd, Pleasanton, California, 94588', '(925) 847-6000', 'open')
  
SELECT * FROM customer WHERE id = 'abc'' DROP TABLE customer; --'

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.