Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defer execution for ES6 Template Literals

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?

like image 947
CodingIntrigue Avatar asked Mar 24 '14 11:03

CodingIntrigue


People also ask

Are template literals ES6?

Template Literals is an ES6 feature (JavaScript 2015). Template Literals is not supported in Internet Explorer.

How do you use backticks in template literals?

In that case, the template literal is passed to your tag function, where you can then perform whatever operations you want on the different parts of the template literal. To escape a backtick in a template literal, put a backslash ( \ ) before the backtick. Dollar signs can be escaped as well to prevent interpolation.

Does IE11 support template literals?

If you look at the ECMAScript 6 compatibility table, you'll see that template literals are not supported by IE11.

Why backticks are used in JavaScript?

Backticks are an ES6 feature that allows you to create strings in JavaScript. Although backticks are mostly used for HTML or code embedding purposes, they also act similar to single and double quotes. Besides, using backticks makes it easier for string operations.


2 Answers

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: ^               ^ 
like image 197
Bergi Avatar answered Sep 21 '22 19:09

Bergi


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); } 

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.

like image 27
Rodrigo Rodrigues Avatar answered Sep 23 '22 19:09

Rodrigo Rodrigues