223

Is it possible to create a template string as a usual string,

let a = "b:${b}";

and then convert it into a template string,

let b = 10;
console.log(a.template()); // b:10

without eval, new Function and other means of dynamic code generation?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
KOLANICH
  • 2,904
  • 2
  • 20
  • 20
  • 9
    did you find a way to achieve this? I might be needing to do it one day and am curious to know what you've arrived at. – Bryan Rayner Aug 13 '15 at 23:17
  • 1
    @BryanRayner lets say your js program is trying to fetch a data from rest API, whose url is in a config.js file as a string "/resources//update/" and you put "resource_id" dynamically from your program. Unless you want to split that url into parts and save in different areas, you need some sort of string template processing. – Ryu_hayabusa Dec 27 '16 at 05:55
  • 2
    https://stackoverflow.com/questions/57565794/how-would-you-turn-a-javascript-variable-into-a-template-literal – Trevor Aug 20 '19 at 04:45
  • Instead of using eval better is use to regex Eval it's not recommended & highly discouraged, so please don't use it developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…! let b = 10; let a="b:${b}"; let response = a.replace(/\${\w+}/ ,b); conssole.log(response); – Vijay Palaskar Jul 16 '20 at 12:28
  • @Ryu_hayabusa I believe the goal here is specifically to be able to reference these variable values __without__ template preprocessing so that we can manipulate with them live rather than only on the server-side. – Brandon McConnell Jun 03 '21 at 15:33
  • @KOLANICH to second Bryan's question, did you ever manage to figure this out? I need this for a project I am working on right now. I am currently using `new Function()` but would prefer not to in light of XSS concerns. – Brandon McConnell Jun 03 '21 at 15:34
  • i have this problem too – amin mahjoob Aug 22 '21 at 15:49
  • Related: [Defer execution for ES6 Template Literals](/q/22607806/4642212). – Sebastian Simon Oct 19 '22 at 19:25

23 Answers23

164

In my project I've created something like this with ES6:

String.prototype.interpolate = function(params) {
  const names = Object.keys(params);
  const vals = Object.values(params);
  return new Function(...names, `return \`${this}\`;`)(...vals);
}

const template = 'Example text: ${text}';
const result = template.interpolate({
  text: 'Foo Boo'
});
console.log(result);
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mateusz Moska
  • 1,921
  • 1
  • 14
  • 18
  • 2
    Hi, Your solution works great, but when i used it in React Native(build mode), it throws an error: **Invalid character '`'**, though it works when i run in debug mode. Looks like, babel issue, any help? – Mohit Pandey May 22 '17 at 17:55
  • 1
    @MohitPandey I was getting same error when I was running tests of this code under PhantomJS and it was passing under chrome. If that is the case, I think there is new beta version of PhantomJS on the way with better support for ES6, you can try to install it. – Mateusz Moska May 23 '17 at 11:11
  • 1
    Unfortunately, it doesn't work and i wrote down a regex for the same. Added as answer as well. – Mohit Pandey May 23 '17 at 15:16
  • 2
    this solution only works if the back-tick "`" character is not present in the template string – SliverNinja - MSFT Sep 19 '17 at 19:21
  • When I try it I got [`ReferenceError: _ is not defined`](https://jsbin.com/xawutog/1/edit?js,console,output). Is it code not ES6 but `lodash` specific, or...? – xpt Feb 19 '18 at 20:53
  • @xpt thanks for your comment. There was lodash dependency indeed, I've removed it, check the update. – Mateusz Moska Feb 21 '18 at 09:50
  • Works like a charm. Thanks Mateusz! – xpt Feb 21 '18 at 19:07
  • 1
    It throws error 'SyntaxError: Octal escape sequences are not allowed in strict mode.' when there is back slash in template. – aleung Jun 15 '18 at 06:14
  • @aleung, this solution is definitely not fully bulletproof, can be some edge cases where it's throwing exceptions. Maybe try double escape backslash. You can put there also some protection to escape critical chars, like backslash or back-tick – Mateusz Moska Jun 15 '18 at 13:46
  • does not work if json replacer contains integer value – uray Dec 07 '18 at 12:30
  • 1
    This is a _very_ interesting alternative to using `with (params) eval('\`'+str.replace(/\`/g,'\\\`')+'\`');`. – Roy Tinker Aug 09 '19 at 23:58
  • 1
    Great solution! Note that this works with passing nested objects as the parameter as well! Here's an example: given the nested object: `parent = {child: {a: "1", b: "2"}} ` and the string literal: `s = "${parent.child.a}${parent.child.b}${parent.child.a}${parent.child.b}"` then `s.interpolate(parent)` will return `"1212"` – Logan Besecker Oct 07 '19 at 18:22
  • If you got this error `Arg string terminates parameters early` check this answer https://stackoverflow.com/a/59410199/6478359 – Muhammet Can TONBUL Dec 19 '19 at 12:53
  • This solution is far better than eval. eval should be banned. – Benjam Jun 03 '20 at 21:05
  • This solution works great for me, but it looks a bit hacky. Is it safe to use it in prodaction? – L. Langó Jun 26 '20 at 14:07
  • 1
    Hmmm..... I try to do this: `var cloneHtml = clone.outerHTML.interpolate(viewerData);` and get `TypeError: Cannot read property 'interpolate' of undefined`. I've declared `String.prototype.interpolate` exactly as shown above.... – Antonio Ooi Aug 08 '20 at 08:38
  • 2
    Sorry guys, it works now: `document.querySelector("template").innerHTML.interpolate(viewerData);` – Antonio Ooi Aug 08 '20 at 09:27
  • This is awesome, can you explain or your provide any reference to understand this ..... `return new Function(...names, \`return \`${this}\`;\`)(...vals);` – mukuljainx Apr 04 '21 at 11:25
  • 2
    @mukuljainx Check how does 'new Function' is working: https://javascript.info/new-function. What above example is basically doing is this: `(new Function('text', 'return \`Example text: ${text}\`;'))('Foo Boo');` – Mateusz Moska Apr 05 '21 at 13:08
  • 4
    It's worth nothing that while this is an improvement over `eval()` because it doesn't leak your scope, the [`Function()` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function) _does_ share some security issues with `eval()`, as it is still evaluating the string as JavaScript, and should be treated with similar caution. MDN's documentation on [never use eval()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_eval!) goes into this in greater detail. – cincodenada Apr 09 '21 at 23:03
  • 2
    Yes, and to further support @cincodenada's comment, any browsers or webservers blocking XSS tend to block both, `eval()` and `new Function()`. – Brandon McConnell Jun 03 '21 at 14:07
  • 2
    That should be best answer, but there are security reason for avoid `eval()` and `new Function()` specially in the browsers. – OzzyCzech Aug 02 '21 at 08:45
  • Please beware that changing `function(params)` to an arrow function `params =>` may break this for some reason, at least for my use case changing to arrow function broke it and it would always output `"[object Object]"`. – user5507535 Jan 23 '22 at 10:39
  • works with passing in functions too! ```function interpolate(input, params) { const names = Object.keys(params); const vals = Object.values(params); return new Function(...names, `return \`${input}\`;`)(...vals); } function something(t) { if (t) return "better than nothing" + t; return "nada"; } const template = 'Example text: ${text} or ${r} or ${s("test")}'; const result = interpolate(template, { r: 'dog', text: 'Foo Boo', s: something }); console.log(result); ``` – Brad Parks Jan 09 '23 at 12:02
121

As your template string must get reference to the b variable dynamically (in runtime), so the answer is: NO, it's impossible to do it without dynamic code generation.

But, with eval it's pretty simple:

let tpl = eval('`'+a+'`');
ataravati
  • 8,891
  • 9
  • 57
  • 89
alexpods
  • 47,475
  • 10
  • 100
  • 94
  • 10
    eval is insecure, so is other means of dynamic code generation – KOLANICH Mar 22 '15 at 09:53
  • 10
    @KOLANICH For particular that case - escape back quotes in the `a` string and it will be much less insecure: `let tpl = eval('\`'+a.replace(/\`/g,'\\\`')+'\`');`. I think more important is that `eval` prevent compiler to optimize your code. But I think it irrelevant to this question. – alexpods Mar 22 '15 at 10:10
  • But template strings are not meant to be constructed by user, so the security of this method can be OK. But in any case I dislike it. – KOLANICH Mar 22 '15 at 10:12
  • 4
    In fact, you can also run functions inside template strings. – KOLANICH Mar 22 '15 at 10:15
  • @KOLANICH Yes. But when you wrote the question shouldn't you suggest that: `let a = "b:${sumFunc()}"; console.log(a.template());` will run `sumFunc` function when template string are created? – alexpods Mar 22 '15 at 10:20
  • In fact I hadn't expected this behaviour when started the question. – KOLANICH Mar 22 '15 at 10:38
  • 11
    @KOLANICH Sorry you dislike `eval`. However, remember that a template literal is itself a form of `eval`. Two examples: var test=`Result: ${alert('hello')}`; var test=`Result: ${b=4}`; Both will end up executing arbitrary code in the context of the script. If you want to allow arbitrary strings, you may as well as allow `eval`. – Manngo Aug 14 '16 at 02:02
  • 6
    Be careful. Since something like babel won't transpile this, this code will NOT work in IE – cgsd Jan 09 '17 at 20:39
  • 3
    you can shortcut it a little if you add `` in the original string in advance : example `let string = '\`my is ${name}\`'` – pery mimon Mar 11 '18 at 13:51
  • Instead of using eval better is use to regex Eval it's not recommended & highly discouraged, so please don't use it https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#Never_use_eval! let b = 10; let a="b:${b}"; let response = a.replace(/\${\w+}/ ,b); conssole.log(response); – Vijay Palaskar Jul 16 '20 at 10:33
  • It is highly unsecure, be carefull: _const templateJsInjection = '1` && (console.log("injected") || 1) && `1'_ – Paweł Siemienik Feb 11 '21 at 19:52
33

No, there is not a way to do this without dynamic code generation.

However, I have created a function which will turn a regular string into a function which can be provided with a map of values, using template strings internally.

Generate Template String Gist

/**
 * Produces a function which uses template strings to do simple interpolation from objects.
 * 
 * Usage:
 *    var makeMeKing = generateTemplateString('${name} is now the king of ${country}!');
 * 
 *    console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'}));
 *    // Logs 'Bryan is now the king of Scotland!'
 */
var generateTemplateString = (function(){
    var cache = {};

    function generateTemplate(template){
        var fn = cache[template];

        if (!fn){
            // Replace ${expressions} (etc) with ${map.expressions}.

            var sanitized = template
                .replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){
                    return `\$\{map.${match.trim()}\}`;
                    })
                // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
                .replace(/(\$\{(?!map\.)[^}]+\})/g, '');

            fn = Function('map', `return \`${sanitized}\``);
        }

        return fn;
    }

    return generateTemplate;
})();

Usage:

var kingMaker = generateTemplateString('${name} is king!');

console.log(kingMaker({name: 'Bryan'}));
// Logs 'Bryan is king!' to the console.

Hope this helps somebody. If you find a problem with the code, please be so kind as to update the Gist.

Matt Browne
  • 12,169
  • 4
  • 59
  • 75
Bryan Rayner
  • 4,172
  • 4
  • 26
  • 38
  • Thanks! I used this instead of a javascript sprintf solution. – seangwright Apr 13 '16 at 22:24
  • 1
    doesn't work for every templates `var test = generateTemplateString('/api/${param1}/${param2}/')` `console.log(test({param1: 'bar', param2: 'foo'}))` return `/api/bar//` – Guillaume Vincent Jul 29 '16 at 05:41
  • Thanks, fixed. The regex was including a single match of ${param1}/${param2} when it should have been two matches. – Bryan Rayner Jul 29 '16 at 14:21
  • Note that this one doesn't work in IE11, because of missing support for back ticks. – s.meijer Jan 17 '17 at 12:07
  • 1
    Of course, if template strings aren't supported by a browser, this method won't work. If you want to use template strings in unsupported browsers, I would recommend using a language like TypeScript, or a transpiler like Babel; That's the only way to get ES6 into old browsers. – Bryan Rayner Jan 17 '17 at 13:31
  • Note that this would not pass the ESLint rules in some codebases - specifically if the [no-new-func](https://eslint.org/docs/rules/no-new-func) rule is enabled. The `Function` constructor works a bit differently (i.e. is a little safer) than `eval`, but some programmers prefer to avoid it completely for similar reasons (debugging, security). – Matt Browne Nov 17 '17 at 18:07
  • A disclaimer - the solution I proposed _works_ - but that doesn't mean I recommend it. At the point that you're trying to get dynamic templates into your app, you're better off building your own DSL in my opinion. – Bryan Rayner Nov 27 '17 at 22:55
  • Working like a chamr ! – Jasson Rojas Aug 08 '18 at 17:32
31

What you're asking for here:

//non working code quoted from the question
let b=10;
console.log(a.template());//b:10

is exactly equivalent (in terms of power and, er, safety) to eval: the ability to take a string containing code and execute that code; and also the ability for the executed code to see local variables in the caller's environment.

There is no way in JS for a function to see local variables in its caller, unless that function is eval(). Even Function() can't do it.


When you hear there's something called "template strings" coming to JavaScript, it's natural to assume it's a built-in template library, like Mustache. It isn't. It's mainly just string interpolation and multiline strings for JS. I think this is going to be a common misconception for a while, though. :(

aWebDeveloper
  • 36,687
  • 39
  • 170
  • 242
Jason Orendorff
  • 42,793
  • 6
  • 62
  • 96
19

There are many good solutions posted here, but none yet which utilizes the ES6 String.raw method. Here is my contriubution. It has an important limitation in that it will only accept properties from a passed in object, meaning no code execution in the template will work.

function parseStringTemplate(str, obj) {
    let parts = str.split(/\$\{(?!\d)[\wæøåÆØÅ]*\}/);
    let args = str.match(/[^{\}]+(?=})/g) || [];
    let parameters = args.map(argument => obj[argument] || (obj[argument] === undefined ? "" : obj[argument]));
    return String.raw({ raw: parts }, ...parameters);
}
let template = "Hello, ${name}! Are you ${age} years old?";
let values = { name: "John Doe", age: 18 };

parseStringTemplate(template, values);
// output: Hello, John Doe! Are you 18 years old?
  1. Split string into non-argument textual parts. See regex.
    parts: ["Hello, ", "! Are you ", " years old?"]
  2. Split string into property names. Empty array if match fails.
    args: ["name", "age"]
  3. Map parameters from obj by property name. Solution is limited by shallow one level mapping. Undefined values are substituted with an empty string, but other falsy values are accepted.
    parameters: ["John Doe", 18]
  4. Utilize String.raw(...) and return result.
pekaaw
  • 2,309
  • 19
  • 18
  • 2
    Out of curiosity, what value is String.raw actually providing here? It seems you're doing all the work of parsing the string and keeping track of what the substitions are. Is this much different from simply calling `.replace()` repeatedly? – Steve Bennett May 12 '20 at 01:31
  • Fair point, @SteveBennett. I had some problems turning a normal string into a template string, and found a solution by building the raw object myself. I guess it reduces String.raw to a concatenation method, but I think it works quite well. I would like to see a nice solution with ```.replace()```, though :) I think readability is important, so while using regular expressions myself, I try to name them to help make sense of it all... – pekaaw May 13 '20 at 09:02
  • 1
    Yeah works like charm. Tested on React native as well. Thanks! – Ali Abbas Sep 20 '22 at 06:29
13

TLDR: https://jsfiddle.net/bj89zntu/1/

Everyone seems to be worried about accessing variables. Why not just pass them? I'm sure it won't be too hard to get the variable context in the caller and pass it down. Use ninjagecko's answer to get the props from obj.

function renderString(str,obj){
    return str.replace(/\$\{.+?\}/g,(match)=>{return index(obj,match)})
}

Here is the full code:

function index(obj,is,value) {
    if (typeof is == 'string')
        is=is.split('.');
    if (is.length==1 && value!==undefined)
        return obj[is[0]] = value;
    else if (is.length==0)
        return obj;
    else
        return index(obj[is[0]],is.slice(1), value);
}

function renderString(str,obj){
    return str.replace(/\$\{.+?\}/g,(match)=>{return index(obj,match)})
}

renderString('abc${a}asdas',{a:23,b:44}) //abc23asdas
renderString('abc${a.c}asdas',{a:{c:22,d:55},b:44}) //abc22asdas
M3D
  • 673
  • 6
  • 11
  • @s.meijer could you elaborate? I am using this code successfully. https://jsfiddle.net/w3jx07vt/ – M3D Jan 19 '17 at 01:25
  • 1
    Better regex would allow you to not select the `${}` characters. Try: `/(?!\${)([^{}]*)(?=})/g` – Eric Hodonsky Jul 03 '17 at 17:34
  • @Relic https://jsfiddle.net/w3jx07vt/2/ I couldn't get that working, care to lend a hand and I'll update my post? :) – M3D Jul 05 '17 at 02:54
  • So the way you're trying to grab it this won't actually help much, I ended up doing a string replace instead. Instead of adding a step of interpolation, so I can use the string as interp or string. Not fancy, but it worked. – Eric Hodonsky Jul 06 '17 at 23:13
  • @EricHodonsky Could you elaborate on "the way I'm trying to grab it"? Perhaps share your use case. Most of the above solutions focus on code generation, which my solution does not require. The problem here is A) Accessing the variables (which I do not do, letting the user pass them in instead), B) finding where to substitute names, C) looking up names (on an object, in this case). This works according to jsfiddle. – M3D Jan 10 '21 at 05:50
  • The fiddle code works great but the code listed in the answer here on stackoverflow does not. For some reason they're different? – patorjk Mar 20 '23 at 13:41
10

The issue here is to have a function that has access to the variables of its caller. This is why we see direct eval being used for template processing. A possible solution would be to generate a function taking formal parameters named by a dictionary's properties, and calling it with the corresponding values in the same order. An alternative way would be to have something simple as this:

var name = "John Smith";
var message = "Hello, my name is ${name}";
console.log(new Function('return `' + message + '`;')());

And for anyone using Babel compiler we need to create closure which remembers the environment in which it was created:

console.log(new Function('name', 'return `' + message + '`;')(name));
didinko
  • 402
  • 6
  • 10
  • 1
    Your first snippet is actually worse than `eval` because it works only with a global `name` variable – Bergi Nov 28 '15 at 23:06
  • @Bergi Your statement is valid - the scope of the function will be lost. I wanted to present an easy solution to the problem and provided a simplified example of what can be done. One could simply come up with the following to overcome the problem: `var template = function() { var name = "John Smith"; var message = "Hello, my name is ${name}"; this.local = new Function('return `' + message + '`;')();}` – didinko Nov 29 '15 at 12:49
  • No, that's exactly what would *not* work - `new Function` doesn't have access to the `var name` in the `template` function. – Bergi Nov 29 '15 at 13:49
  • The second snip fixed my problem... Up vote from me! Thanks, this helped solve a temporary problem we were having with dynamic routing to an iframe :) – Kris Boyd Jul 20 '17 at 19:00
  • could you please explain, `anyone using Babel compiler we need to create closure which remembers the environment in which it was created`? – rahul Kushwaha Dec 08 '21 at 09:10
9

I liked s.meijer's answer and wrote my own version based on his:

function parseTemplate(template, map, fallback) {
    return template.replace(/\$\{[^}]+\}/g, (match) => 
        match
            .slice(2, -1)
            .trim()
            .split(".")
            .reduce(
                (searchObject, key) => searchObject[key] || fallback || match,
                map
            )
    );
}
Daniel
  • 8,655
  • 5
  • 60
  • 87
9

Similar to Daniel's answer (and s.meijer's gist) but more readable:

const regex = /\${[^{]+}/g;

export default function interpolate(template, variables, fallback) {
    return template.replace(regex, (match) => {
        const path = match.slice(2, -1).trim();
        return getObjPath(path, variables, fallback);
    });
}

//get the specified property or nested property of an object
function getObjPath(path, obj, fallback = '') {
    return path.split('.').reduce((res, key) => res[key] || fallback, obj);
}

Note: This slightly improves s.meijer's original, since it won't match things like ${foo{bar} (the regex only allows non-curly brace characters inside ${ and }).


UPDATE: I was asked for an example using this, so here you go:

const replacements = {
    name: 'Bob',
    age: 37
}

interpolate('My name is ${name}, and I am ${age}.', replacements)
Matt Browne
  • 12,169
  • 4
  • 59
  • 75
  • Can you post an example actually using this? This javascript is a bit beyond me. I'd suggest a regex of `/\$\{(.*?)(?!\$\{)\}/g` (to handle nest curly braces). I have a working solution but I'm not sure it's as portable as yours, so I'd love to see how this should be implemented in a page. Mine also uses `eval()`. – Regular Jo Jan 17 '18 at 05:08
  • I went ahead and posted an answer as well, and I'd love your feedback on how to make that more secure and performance-minded: https://stackoverflow.com/a/48294208. – Regular Jo Jan 17 '18 at 05:38
  • @RegularJoe I added an example. My goal was to keep it simple, but you're right that if you want to handle nested curly braces, you would need to change the regex. However, I can't think of a use case for that when evaluating a regular string as if it were a template literal (the whole purpose of this function). What did you have in mind? – Matt Browne Jan 17 '18 at 22:25
  • Also, I am neither a performance expert nor a security expert; my answer is really just combining two previous answers. But I will say that using `eval` leaves you a lot more open to possible mistakes that would cause security issues, whereas all my version is doing is looking up a property on an object from a dot-separated path, which should be safe. – Matt Browne Jan 17 '18 at 22:30
6

@Mateusz Moska, solution works great, but when i used it in React Native(build mode), it throws an error: Invalid character '`', though it works when i run it in debug mode.

So i wrote down my own solution using regex.

String.prototype.interpolate = function(params) {
  let template = this
  for (let key in params) {
    template = template.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), params[key])
  }
  return template
}

const template = 'Example text: ${text}',
  result = template.interpolate({
    text: 'Foo Boo'
  })

console.log(result)

Demo: https://es6console.com/j31pqx1p/

NOTE: Since I don't know the root cause of an issue, i raised a ticket in react-native repo, https://github.com/facebook/react-native/issues/14107, so that once they can able to fix/guide me about the same :)

Mohit Pandey
  • 3,679
  • 7
  • 26
  • 38
  • this does support templates containing the back-tick character. However, rather than try to invent a templating pattern, you are probably better off just [using mustache or similar](https://www.npmjs.com/package/mustache). depending on how complex your templates are this is a brute force approach that doesn't consider edge cases - the key could contain a special regex pattern. – SliverNinja - MSFT Sep 19 '17 at 19:26
5

You can use the string prototype, for example

String.prototype.toTemplate=function(){
    return eval('`'+this+'`');
}
//...
var a="b:${b}";
var b=10;
console.log(a.toTemplate());//b:10

But the answer of the original question is no way.

sarkiroka
  • 1,485
  • 20
  • 28
4

I required this method with support for Internet Explorer. It turned out the back ticks aren't supported by even IE11. Also; using eval or it's equivalent Function doesn't feel right.

For the one that notice; I also use backticks, but these ones are removed by compilers like babel. The methods suggested by other ones, depend on them on run-time. As said before; this is an issue in IE11 and lower.

So this is what I came up with:

function get(path, obj, fb = `$\{${path}}`) {
  return path.split('.').reduce((res, key) => res[key] || fb, obj);
}

function parseTpl(template, map, fallback) {
  return template.replace(/\$\{.+?}/g, (match) => {
    const path = match.substr(2, match.length - 3).trim();
    return get(path, map, fallback);
  });
}

Example output:

const data = { person: { name: 'John', age: 18 } };

parseTpl('Hi ${person.name} (${person.age})', data);
// output: Hi John (18)

parseTpl('Hello ${person.name} from ${person.city}', data);
// output: Hello John from ${person.city}

parseTpl('Hello ${person.name} from ${person.city}', data, '-');
// output: Hello John from -
s.meijer
  • 3,403
  • 3
  • 26
  • 23
  • "using eval or it's equivalent Function doesn't feel right." ... Yeah... I agree, but I think this is one of the very few use cases in which one could say, "mmhkay, let's use it". Please check https://jsperf.com/es6-string-tmpl -- it's my practical use case. Using your function (with the same regexp as mine) and mine (eval + string literals). Thanks! :) – Andrea Puddu Feb 21 '17 at 10:46
  • @AndreaPuddu, your performance is indeed better. But then again; template strings are unsupported in IE. So ``eval('`' + taggedURL + '`')`` simply doesn't work. – s.meijer Feb 21 '17 at 11:57
  • "Seems" better I'd say, because it's tested in isolation... The only purpose of that test was to see the potential performance issues using `eval`. Regarding template literals: thanks for pointing that out again. I'm using Babel to transpile my code but my function will still not work apparently – Andrea Puddu Feb 22 '17 at 13:50
3

I currently can't comment on existing answers so I am unable to directly comment on Bryan Raynor's excellent response. Thus, this response is going to update his answer with a slight correction.

In short, his function fails to actually cache the created function, so it will always recreate, regardless of whether it's seen the template before. Here is the corrected code:

    /**
     * Produces a function which uses template strings to do simple interpolation from objects.
     * 
     * Usage:
     *    var makeMeKing = generateTemplateString('${name} is now the king of ${country}!');
     * 
     *    console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'}));
     *    // Logs 'Bryan is now the king of Scotland!'
     */
    var generateTemplateString = (function(){
        var cache = {};

        function generateTemplate(template){
            var fn = cache[template];

            if (!fn){
                // Replace ${expressions} (etc) with ${map.expressions}.

                var sanitized = template
                    .replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){
                        return `\$\{map.${match.trim()}\}`;
                    })
                    // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
                    .replace(/(\$\{(?!map\.)[^}]+\})/g, '');

                fn = cache[template] = Function('map', `return \`${sanitized}\``);
            }

            return fn;
        };

        return generateTemplate;
    })();
user2501097
  • 1,011
  • 8
  • 4
3

Still dynamic but seems more controlled than just using a naked eval:

const vm = require('vm')
const moment = require('moment')


let template = '### ${context.hours_worked[0].value} \n Hours worked \n #### ${Math.abs(context.hours_worked_avg_diff[0].value)}% ${fns.gt0(context.hours_worked_avg_diff[0].value, "more", "less")} than usual on ${fns.getDOW(new Date())}'
let context = {
  hours_worked:[{value:10}],
  hours_worked_avg_diff:[{value:10}],

}


function getDOW(now) {
  return moment(now).locale('es').format('dddd')
}

function gt0(_in, tVal, fVal) {
  return _in >0 ? tVal: fVal
}



function templateIt(context, template) {
  const script = new vm.Script('`'+template+'`')
  return script.runInNewContext({context, fns:{getDOW, gt0 }})
}

console.log(templateIt(context, template))

https://repl.it/IdVt/3

Robert Moskal
  • 21,737
  • 8
  • 62
  • 86
2

I made my own solution doing a type with a description as a function

export class Foo {
...
description?: Object;
...
}

let myFoo:Foo = {
...
  description: (a,b) => `Welcome ${a}, glad to see you like the ${b} section`.
...
}

and so doing:

let myDescription = myFoo.description('Bar', 'bar');
2

I came up with this implementation and it works like a charm.

function interpolateTemplate(template: string, args: any): string {
  return Object.entries(args).reduce(
    (result, [arg, val]) => result.replace(`$\{${arg}}`, `${val}`),
    template,
  )
}

const template = 'This is an example: ${name}, ${age} ${email}'

console.log(interpolateTemplate(template,{name:'Med', age:'20', email:'example@abc.com'}))

You could raise an error if arg is not found in template

darrachequesne
  • 742
  • 7
  • 18
  • While fine for simple property binding, this doesn't support template nesting or other expressions in the usual way. You have to evaluate any nested templates or nested expressions before passing them to interpolateTemplate. – naasking Sep 27 '22 at 15:14
1

This solution works without ES6:

function render(template, opts) {
  return new Function(
    'return new Function (' + Object.keys(opts).reduce((args, arg) => args += '\'' + arg + '\',', '') + '\'return `' + template.replace(/(^|[^\\])'/g, '$1\\\'') + '`;\'' +
    ').apply(null, ' + JSON.stringify(Object.keys(opts).reduce((vals, key) => vals.push(opts[key]) && vals, [])) + ');'
  )();
}

render("hello ${ name }", {name:'mo'}); // "hello mo"

Note: the Function constructor is always created in the global scope, which could potentially cause global variables to be overwritten by the template, e.g. render("hello ${ someGlobalVar = 'some new value' }", {name:'mo'});

cruzanmo
  • 747
  • 8
  • 8
1

You should try this tiny JS module, by Andrea Giammarchi, from github : https://github.com/WebReflection/backtick-template

/*! (C) 2017 Andrea Giammarchi - MIT Style License */
function template(fn, $str, $object) {'use strict';
  var
    stringify = JSON.stringify,
    hasTransformer = typeof fn === 'function',
    str = hasTransformer ? $str : fn,
    object = hasTransformer ? $object : $str,
    i = 0, length = str.length,
    strings = i < length ? [] : ['""'],
    values = hasTransformer ? [] : strings,
    open, close, counter
  ;
  while (i < length) {
    open = str.indexOf('${', i);
    if (-1 < open) {
      strings.push(stringify(str.slice(i, open)));
      open += 2;
      close = open;
      counter = 1;
      while (close < length) {
        switch (str.charAt(close++)) {
          case '}': counter -= 1; break;
          case '{': counter += 1; break;
        }
        if (counter < 1) {
          values.push('(' + str.slice(open, close - 1) + ')');
          break;
        }
      }
      i = close;
    } else {
      strings.push(stringify(str.slice(i)));
      i = length;
    }
  }
  if (hasTransformer) {
    str = 'function' + (Math.random() * 1e5 | 0);
    if (strings.length === values.length) strings.push('""');
    strings = [
      str,
      'with(this)return ' + str + '([' + strings + ']' + (
        values.length ? (',' + values.join(',')) : ''
      ) + ')'
    ];
  } else {
    strings = ['with(this)return ' + strings.join('+')];
  }
  return Function.apply(null, strings).apply(
    object,
    hasTransformer ? [fn] : []
  );
}

template.asMethod = function (fn, object) {'use strict';
  return typeof fn === 'function' ?
    template(fn, this, object) :
    template(this, fn);
};

Demo (all the following tests return true):

const info = 'template';
// just string
`some ${info}` === template('some ${info}', {info});

// passing through a transformer
transform `some ${info}` === template(transform, 'some ${info}', {info});

// using it as String method
String.prototype.template = template.asMethod;

`some ${info}` === 'some ${info}'.template({info});

transform `some ${info}` === 'some ${info}'.template(transform, {info});
colxi
  • 7,640
  • 2
  • 45
  • 43
1

Faz assim (This way):

let a = 'b:${this.b}'
let b = 10

function template(templateString, templateVars) {
    return new Function('return `' + templateString + '`').call(templateVars)
}

result.textContent = template(a, {b})
<b id=result></b>
Thiago Lagden
  • 161
  • 1
  • 4
  • Another example: ```const dados = {nome: 'Marquinhos', email: 'xiii@marquinhos.com'};template('${this.nome} utiliza esse email: ${this.email}!', dados)``` – Thiago Lagden Apr 08 '22 at 17:58
1

You can refer to this solution

const interpolate = (str) =>
  new Function(`return \`${new String(str)}\`;`)();

const foo = 'My';

const obj = {
  text: 'Hanibal Lector',
  firstNum: 1,
  secondNum: 2
}
 
const str = "${foo} name is : ${obj.text}. sum = ${obj.firstNum} + ${obj.secondNum} = ${obj.firstNum + obj.secondNum}";

console.log(interpolate(str));

by returning a new function that has a template literal that copies the full string into that template literal you achieve the correct variable expansion wanted - this is useful if you want to have strings saved in some content management system that you then want to expand.

The code also includes an example of this working.

user254694
  • 1,461
  • 2
  • 23
  • 46
  • 1
    It's important to not just post code, but to also include a description of what the code does and why you are suggesting it. This helps others understand the context and purpose of the code, and makes it more useful for others who may be reading the question or answer. – DSDmark Dec 29 '22 at 17:47
1

Option1:

function replaceTemplate(templateString, objContext) {
        return templateString.replace(/\${[A-Za-z0-9_\$]+}/ig, (data) => { const key = data.replace(/^\${/, '').replace(/}$/, ''); return objContext[key] || data })
     }

or

Option2:

function replaceTemplate(templateString) {
    return templateString.replace(/\${[A-Za-z0-9_\$]+}/ig, (data) => { const key = data.replace(/^\${/, '').replace(/}$/, ''); return this[key] || data })
}

example

const fruits = { apple: 1, banana: 3, stringValue: 'a: ${apple}, b: ${banana}'} 
replaceTemplate(fruits.stringValue, fruits) // Option1: 
// or 
replaceTemplate.bind(fruits)(fruits.stringValue) //Option2: 

output: 'apple: 1, banana: 3, orange: ${orange}'
-1

Since we're reinventing the wheel on something that would be a lovely feature in javascript.

I use eval(), which is not secure, but javascript is not secure. I readily admit that I'm not excellent with javascript, but I had a need, and I needed an answer so I made one.

I chose to stylize my variables with an @ rather than an $, particularly because I want to use the multiline feature of literals without evaluating til it's ready. So variable syntax is @{OptionalObject.OptionalObjectN.VARIABLE_NAME}

I am no javascript expert, so I'd gladly take advice on improvement but...

var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g
for(i = 0; i < myResultSet.length; i++) {
    prsLiteral = rt.replace(prsRegex,function (match,varname) {
        return eval(varname + "[" + i + "]");
        // you could instead use return eval(varname) if you're not looping.
    })
    console.log(prsLiteral);
}

A very simple implementation follows

myResultSet = {totalrecords: 2,
Name: ["Bob", "Stephanie"],
Age: [37,22]};

rt = `My name is @{myResultSet.Name}, and I am @{myResultSet.Age}.`

var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g
for(i = 0; i < myResultSet.totalrecords; i++) {
    prsLiteral = rt.replace(prsRegex,function (match,varname) {
        return eval(varname + "[" + i + "]");
        // you could instead use return eval(varname) if you're not looping.
    })
    console.log(prsLiteral);
}

In my actual implementation, I choose to use @{{variable}}. One more set of braces. Absurdly unlikely to encounter that unexpectedly. The regex for that would look like /\@\{\{(.*?)(?!\@\{\{)\}\}/g

To make that easier to read

\@\{\{    # opening sequence, @{{ literally.
(.*?)     # capturing the variable name
          # ^ captures only until it reaches the closing sequence
(?!       # negative lookahead, making sure the following
          # ^ pattern is not found ahead of the current character
  \@\{\{  # same as opening sequence, if you change that, change this
)
\}\}      # closing sequence.

If you're not experienced with regex, a pretty safe rule is to escape every non-alphanumeric character, and don't ever needlessly escape letters as many escaped letters have special meaning to virtually all flavors of regex.

Regular Jo
  • 5,190
  • 3
  • 25
  • 47
-4

I realize I am late to the game, but you could:

const a =  (b) => `b:${b}`;

let b = 10;
console.log(a(b)); // b:10
joehep
  • 136
  • 1
  • 5