Search code examples
javascriptlittagged-templates

Interpolating a normal value within a tagged template string in JS


Javascript tagged template strings like

html`<h1>hello ${name}</h1>`;

are really fantastic ways to do interesting things with each interpolated variable without them just becoming part of the string. Unlike untagged templates such as

`<h1>hello ${name}</h1>;

which if const name = 'Mary'; had been set would yield a string indistinguishable from '<h1>hello Mary</h1>'. In the tagged template, the variables are kept separately so they can be manipulated later. Lit (lit-element, lit-html) uses them extensively and normally they're great.

Sometimes, however, one would like to put in a variable as if it were normal text and not trigger the interpolation. One example would be something like:

const main_site_header_level = 'h1';
return html`<$${main_site_header_level}>hello ${name}</$${main_site_header_level}>`;

Where I'm using the (non-existent) $${variable} to indicate to perform the interpolation as if it is just a normal backquote string.

Is there any way to do something like this? It goes against the norms of what tagged literals are for, but it is occasionally very useful.


Solution

  • You seem to want two levels of template processing: Certain values are replaced "immediately", and the rest is then handed over to the tag function (html in your case).

    The immediate function in the following code takes a tag function as argument and returns another tag function which passes only the "non-immediate" values to the given tag function. Instead of the (non-existing) notation $${variable}, this uses ${{immediate:variable}}.

    function html(template, ...values) {
      console.log(template, ...values);
      return "";
    }
    function immediate(f) {
      return function(template, ...values) {
        const v = [];
        const raw = [...template.raw];
        template = [...template];
        for (let i = 0, j = 0; i < values.length; i++) {
          if (values[i] && values[i].immediate !== undefined) {
            template.splice(j, 2, template[j] + `${values[i].immediate}` + template[j + 1]);
            raw.splice(j, 2, raw[j] + `${values[i].immediate}` + raw[j + 1]);
          } else {
            v.push(values[i]);
            j++;
          }
        }
        template.raw = raw;
        return f(template, ...v);
      };
    }
    const main_site_header_level = 'h1';
    const name = 'world';
    html`<${main_site_header_level}>hello ${name}</${main_site_header_level}>`;
    immediate(html)`<${{immediate:main_site_header_level}}>hello ${name}</${{immediate:main_site_header_level}}>`;
    

    When this is executed, it outputs

    [ '<', '>hello ', '</', '>' ] h1 world h1  // seen by the tag function html
    [ '<h1>hello ', '</h1>' ] world  // seen by the tag function immediate(html)
    

    (After writing this, it seems that it duplicates the author's own answer.)