59

I am playing with the new ES6 Template Literals feature and the first thing that came to my head was a String.format for JavaScript so I went about implementing a prototype:

String.prototype.format = function() {
  var self = this;
  arguments.forEach(function(val,idx) {
    self["p"+idx] = val;
  });
  return this.toString();
};
console.log(`Hello, ${p0}. This is a ${p1}`.format("world", "test"));

ES6Fiddle

However, the Template Literal is evaluated before it's passed to my prototype method. Is there any way I can write the above code to defer the result until after I have dynamically created the elements?

Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
CodingIntrigue
  • 75,930
  • 30
  • 170
  • 176
  • Where are you executing this? None of the latest JS implementations don't have this implemented, I think. – thefourtheye Mar 24 '14 at 11:49
  • 1
    @thefourtheye In the ES6Fiddle, linked to in the question – CodingIntrigue Mar 24 '14 at 11:49
  • 1
    I think for a `.format()` method you shouldn't use a template string, but a plain string literal. – Bergi Mar 24 '14 at 11:49
  • @Bergi This is not really meant as a literal problem, more a hypothetical with an example. Seems like having the pre-processed output passed to a function might be a frequent use case – CodingIntrigue Mar 24 '14 at 11:51
  • 2
    It's worth pointing out that backtick strings are simply syntactic sugar for string concatenation and expression evaluation. `\`foo ${5+6}\`` evaluates as `"foo 11"` Attaching a format method to the string prototype would allow you to do silly things like: `\`My ${5+6}th token is {0}\`.format(11)` Which should evaluate as `"My 11th token is 11"`. – Hovis Biddle Aug 11 '15 at 18:11
  • Related: [Convert a string to a template string](/q/29182244/4642212). – Sebastian Simon Oct 19 '22 at 19:25

8 Answers8

71

I can see three ways around this:

  • Use template strings like they were designed to be used, without any format function:

    console.log(`Hello, ${"world"}. This is a ${"test"}`);
    // might make more sense with variables:
    var p0 = "world", p1 = "test";
    console.log(`Hello, ${p0}. This is a ${p1}`);
    

    or even function parameters for actual deferral of the evaluation:

    const welcome = (p0, p1) => `Hello, ${p0}. This is a ${p1}`;
    console.log(welcome("world", "test"));
    
  • Don't use a template string, but a plain string literal:

    String.prototype.format = function() {
        var args = arguments;
        return this.replace(/\$\{p(\d)\}/g, function(match, id) {
            return args[id];
        });
    };
    console.log("Hello, ${p0}. This is a ${p1}".format("world", "test"));
    
  • Use a tagged template literal. Notice that the substitutions will still be evaluated without interception by the handler, so you cannot use identifiers like p0 without having a variable named so. This behavior may change if a different substitution body syntax proposal is accepted (Update: it was not).

    function formatter(literals, ...substitutions) {
        return {
            format: function() {
                var out = [];
                for(var i=0, k=0; i < literals.length; i++) {
                    out[k++] = literals[i];
                    out[k++] = arguments[substitutions[i]];
                }
                out[k] = literals[i];
                return out.join("");
            }
        };
    }
    console.log(formatter`Hello, ${0}. This is a ${1}`.format("world", "test"));
    // Notice the number literals: ^               ^
    
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I like this because it allows you to manipulate the values before they're interpolated. E.g., if you pass in an array of names, you could smartly combine them into strings such as "James", "James & Mary", or "James, Mary, & William" depending on how many are in the array. – Andrew Hedges Sep 29 '16 at 00:27
  • This could also be added as a static method to String like `String.formatter`. – Quentin Engles Oct 29 '16 at 01:36
  • Very thorough. Please refer to @rodrigorodrigues's answer below, particularly his first code block, for the most concise solution. – cage rattler Aug 20 '20 at 15:54
  • Nice. The latter version is almost identical to my own solution to this: https://github.com/spikesagal/es6interpolate/blob/main/src/interpolate.js (also pasted as plain-text to this thread). – Spike Sagal Oct 02 '20 at 03:59
9

Extending @Bergi 's answer, the power of tagged template strings reveals itself when you realize you can return anything as a result, not only plain strings. In his example, the tag constructs and returns an object with a closure and function property format.

In my favorite approach, I return a function value by itself, that you can call later and pass new parameters to fill the template. Like this:

function fmt([fisrt, ...rest], ...tags) {
  return values => rest.reduce((acc, curr, i) => {
    return acc + values[tags[i]] + curr;
  }, fisrt);
}

Or, for the code golfers:

let fmt=([f,...r],...t)=>v=>r.reduce((a,c,i)=>a+v[t[i]]+c,f)

Then you construct your templates and defer the substitutions:

> fmt`Test with ${0}, ${1}, ${2} and ${0} again`(['A', 'B', 'C']);
// 'Test with A, B, C and A again'
> template = fmt`Test with ${'foo'}, ${'bar'}, ${'baz'} and ${'foo'} again`
> template({ foo:'FOO', bar:'BAR' })
// 'Test with FOO, BAR, undefined and FOO again'

Another option, closer to what you wrote, would be to return a object extended from a string, to get duck-typing out of the box and respect interface. An extension to the String.prototype wouldn't work because you'd need the closure of the template tag to resolve the parameters later.

class FormatString extends String {
  // Some other custom extensions that don't need the template closure
}

function fmt([fisrt, ...rest], ...tags) {
  const str = new FormatString(rest.reduce((acc, curr, i) => `${acc}\${${tags[i]}}${curr}`, fisrt));
  str.format = values => rest.reduce((acc, curr, i) => {
    return acc + values[tags[i]] + curr;
  }, fisrt);
  return str;
}

Then, in the call-site:

> console.log(fmt`Hello, ${0}. This is a ${1}.`.format(["world", "test"]));
// Hello, world. This is a test.
> template = fmt`Hello, ${'foo'}. This is a ${'bar'}.`
> console.log(template)
// { [String: 'Hello, ${foo}. This is a ${bar}.'] format: [Function] }
> console.log(template.format({ foo: true, bar: null }))
// Hello, true. This is a null.

You can refer to more information and applications in this other answer.

Rodrigo Rodrigues
  • 7,545
  • 1
  • 24
  • 36
  • 1
    That little reducer is very powerful. Created two Codepens to show sample usage with markup, [one with value objects](https://codepen.io/cagerattler/pen/LYNRrqE?editors=1111) and [one with value arrays](https://codepen.io/cagerattler/pen/JjXRLxN). – cage rattler Aug 20 '20 at 15:41
  • i've been trying to pickup javascript + modern webdev lately and i stumbled upon this reply while exploring the idea of writing a functor-of-sorts for generating fetch hooks dynamically off of API endpoints and param values and i am incredibly impressed by this reducer trick. i have only one question that i haven't been able to figure out with google. what is `values` here? is it related to Object.values()? i have been playing with your first solution in the dev console hasn't made it clear how this value works or where it comes from. – zaile Apr 17 '22 at 22:01
  • 1
    `fmt` is a function that, when evaluated, returns another function. In that snippet, it returns an anonymous function, whose only parameter is named `values`. Note the syntax: `return values => ...`. In this returned function, the parameter `values` is expected to pass a lookup list or object with the substitutions. – Rodrigo Rodrigues Apr 17 '22 at 22:18
3

AFAIS, the useful feature "deferred execution of string templates" is still not available. Using a lambda is an expressive, readable and short solution, however:

var greetingTmpl = (...p)=>`Hello, ${p[0]}. This is a ${p[1]}`;

console.log( greetingTmpl("world","test") );
console.log( greetingTmpl("@CodingIntrigue","try") );
rplantiko
  • 2,698
  • 1
  • 22
  • 21
3

You can inject values into string using below function

let inject = (str, obj) => str.replace(/\${(.*?)}/g, (x,g)=> obj[g]);

let inject = (str, obj) => str.replace(/\${(.*?)}/g, (x,g)=> obj[g]);


// --- Examples ---

// parameters in object
let t1 = 'My name is ${name}, I am ${age}. My brother name is also ${name}.';
let r1 = inject(t1, {name: 'JOHN',age: 23} );
console.log("OBJECT:", r1);


// parameters in array
let t2 = "Today ${0} saw ${2} at shop ${1} times - ${0} was haapy."
let r2 = inject(t2, {...['JOHN', 6, 'SUsAN']} );
console.log("ARRAY :", r2);
Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
1

I also like the idea of the String.format function, and being able to explicitly define the variables for resolution.

This is what I came up with... basically a String.replace method with a deepObject lookup.

const isUndefined = o => typeof o === 'undefined'

const nvl = (o, valueIfUndefined) => isUndefined(o) ? valueIfUndefined : o

// gets a deep value from an object, given a 'path'.
const getDeepValue = (obj, path) =>
  path
    .replace(/\[|\]\.?/g, '.')
    .split('.')
    .filter(s => s)
    .reduce((acc, val) => acc && acc[val], obj)

// given a string, resolves all template variables.
const resolveTemplate = (str, variables) => {
  return str.replace(/\$\{([^\}]+)\}/g, (m, g1) =>
            nvl(getDeepValue(variables, g1), m))
}

// add a 'format' method to the String prototype.
String.prototype.format = function(variables) {
  return resolveTemplate(this, variables)
}

// setup variables for resolution...
var variables = {}
variables['top level'] = 'Foo'
variables['deep object'] = {text:'Bar'}
var aGlobalVariable = 'Dog'

// ==> Foo Bar <==
console.log('==> ${top level} ${deep object.text} <=='.format(variables))

// ==> Dog Dog <==
console.log('==> ${aGlobalVariable} ${aGlobalVariable} <=='.format(this))

// ==> ${not an object.text} <==
console.log('==> ${not an object.text} <=='.format(variables))

Alternatively, if you want more than just variable resolution (e.g. the behaviour of template literals), you can use the following.

N.B. eval is considered 'evil' - consider using a safe-eval alternative.

// evalutes with a provided 'this' context.
const evalWithContext = (string, context) => function(s){
    return eval(s);
  }.call(context, string)

// given a string, resolves all template variables.
const resolveTemplate = function(str, variables) {
  return str.replace(/\$\{([^\}]+)\}/g, (m, g1) => evalWithContext(g1, variables))
}

// add a 'format' method to the String prototype.
String.prototype.format = function(variables) {
  return resolveTemplate(this, variables)
}

// ==> 5Foobar <==
console.log('==> ${1 + 4 + this.someVal} <=='.format({someVal: 'Foobar'}))
Nick Grealy
  • 24,216
  • 9
  • 104
  • 119
0

I posted an answer to a similar question that gives two approaches where the execution of the template literal is delayed. When the template literal is in a function, the template literal is only evaluated when the function is called, and it is evaluated using the scope of the function.

https://stackoverflow.com/a/49539260/188963

abalter
  • 9,663
  • 17
  • 90
  • 145
0

Although this question is already answered, here I have a simple implementations I use when I load configuration files (the code is typescript, but it is very easy to convert into JS, just remove the typings):

/**
 * This approach has many limitations:
 *   - it does not accept variable names with numbers or other symbols (relatively easy to fix)
 *   - it does not accept arbitrary expressions (quite difficult to fix)
 */
function deferredTemplateLiteral(template: string, env: { [key: string]: string | undefined }): string {
  const varsMatcher = /\${([a-zA-Z_]+)}/
  const globalVarsmatcher = /\${[a-zA-Z_]+}/g

  const varMatches: string[] = template.match(globalVarsmatcher) ?? []
  const templateVarNames = varMatches.map(v => v.match(varsMatcher)?.[1] ?? '')
  const templateValues: (string | undefined)[] = templateVarNames.map(v => env[v])

  const templateInterpolator = new Function(...[...templateVarNames, `return \`${template}\`;`])

  return templateInterpolator(...templateValues)
}

// Usage:
deferredTemplateLiteral("hello ${thing}", {thing: "world"}) === "hello world"

Although it's possible to make this stuff more powerful & flexible, it introduces too much complexity and risk without much benefit.

Here a link to the gist: https://gist.github.com/castarco/94c5385539cf4d7104cc4d3513c14f55

castarco
  • 1,368
  • 2
  • 17
  • 33
0

(see @Bergi's very similar answer above)

function interpolate(strings, ...positions) {
  var errors = positions.filter(pos=>~~pos!==pos);
  if (errors.length) {
    throw "Invalid Interpolation Positions: " + errors.join(', ');
  }
  return function $(...vals) {
    var output = '';
    for (let i = 0; i < positions.length; i ++) {
      output += (strings[i] || '') + (vals[positions[i] - 1] || '');
    }
    output += strings[strings.length - 1];
    return output;
  };
}

var iString = interpolate`This is ${1}, which is pretty ${2} and ${3}. Just to reiterate, ${1} is ${2}! (nothing ${0} ${100} here)`;
// Sets iString to an interpolation function

console.log(iString('interpolation', 'cool', 'useful', 'extra'));
// Substitutes the values into the iString and returns:
//   'This is interpolation, which is pretty cool and useful.
//   Just to reiterate, interpolation is cool! (nothing  here)'

The main difference between this and @Bergi's answer is how errors are handled (silently vs not).

It should be easy enough to expand this idea into a syntax that accepts named arguments:

interpolate`This is ${'foo'}, which is pretty ${'bar'}.`({foo: 'interpolation', bar: 'cool'});

https://github.com/spikesagal/es6interpolate/blob/main/src/interpolate.js

Spike Sagal
  • 191
  • 1
  • 4