IDEA: Object.prototype.toTemplate([tagFunction])

I want to be able to take a String and convert the contents to a template literal with a given bound scope.

An example would be to parse the text content of a <template> tag and apply the content as a template literal with a specific scope bound to it.

Given const obj = { thing: "mouse pad"};
I would like to be able to do something equivalent to:
"I need a new ${thing}".toTemplate(obj);
or obj.toTemplate("I need a new ${thing}");

I could see this being something completely different syntax-wise:

  • String.prototype.template([scope])
  • Object.prototype.toTemplate([tagFunction]) - converts and sends the toString to the tag function or sends the untagged converted toString

Kind of seems like what with would do.

It's different, those strings aren't being interpolated, so you're not just pulling variables from the local scope.

For me, it looks more similar to Python's format function. Here's an example of Python's function:

> data = { "x": "my_x", "y": "my_y" }
> "x: {x}, y: {y}".format(**data)
"x: my_x, y: my_y"

However, the only usecase I can think of for something like this, is if the string is being supplied by the end-user (perhaps through a configuration file, or something else). For anything else, you can just create a function to accomplish this purpose:

> const format = ({ x, y }) => `x: ${x}, y: ${y}`
> format({ x: 'my_x', y: 'my_y' })
`x: my_x, y: my_y`

Object.prototype is an unlikely home for any new APIs as this causes too much risk to both new and existing code. A static function would be safer e.g.

Object.template(obj, str)

2 Likes

This seems easy enough to do already, so to meet the bar for adding it to the language you'd want to show it's something which would be very common. I doubt it would be common enough to warrant adding a new API, and almost certainly would not warrant new syntax.

Here's how you'd do this right now:

function toTemplate(strs, ...exprs) {
  return o => String.raw({ raw: strs }, ...exprs.map(e => o[e]));
}

let template = toTemplate`x: ${'x'}, y: ${'y'}`;
console.log(template({ x: 1, y: 2 }));
// 'x: 1, y: 2'

(abusing String.raw while we wait for something better)

1 Like

If you haven’t seen this other proposal before it might be of interest to you: https://github.com/WICG/webcomponents/blob/60c9f682b63c622bfa0d8222ea6b1f3b659e007c/proposals/Template-Instantiation.md

1 Like

These are common responses to this idea when I've presented it elsewhere.

Unfortunately the apparent work arounds and even the Template Instantiation seem to avoid the fact that in order to create a template literal, at this point, you have to start with an existing template literal. We don't have a mechanism available to create a template literal (untagged or tagged) without using the backtick character, which means it is not possible to generate template literals in code.

As near as I can tell every mechanism to create or manipulate a template literal requires that a person physically type that backtick character or create their own syntax parsing within String.raw. Even the .cooked proposal doesn't remove this requirement.

To be honest, I don't understand why the Template Instantiation proposal didn't start with the already powerful template literal mechanisms built into the language. Restricting template parsing to a specific node type seems short-sighted and too narrow.

Simplifying the idea:

Given this string: "${template}" there should be a language-native mechanism to be able to apply or bind a scope to the string that would be equivalent to ` ${template} ` but not require the use of the backtick character.

The benefits of this are pretty great, in my opinion, as it would eliminate the need for the Template Instantiation proposal entirely because one could simply grab the textContent of any element and apply a scope to it, not just <template>.

For a string from an arbitrary source like a <template> element or a loaded file you'll need to do something more like

function template(str, obj) {
    return Function(...Object.keys(obj), `return \`${str}\``)(...Object.values(obj))
}
const str = await loadStr() // "x: ${x}, y: ${y}"
console.log(template(str, { x: 1, y: 2 })); // x: 1, y: 2

It'd be nice to have this option without having to go through an eval to get there.

To avoid eval, libraries like underscore and lodash have template support.

https://lodash.com/docs/#template

Appreciate the responses thus far. I've proposed this idea in other contexts, and the comments are often the same to use String.raw or an additional library.

There is no mechanism within the language to create a template literal (untagged or tagged) without typing the backtick character. The closest would be to use String.raw(...) and write your own parsing.

The Template Instantiation proposal seeks to achieve some great stuff, but is completely focused on the template element. It also goes beyond mere string templating and also handle DOM state management.

I do like the comparison to Python's str.format but think it makes more sense for it to be on the Object because anything with a toString could theoretically also have a corresponding template literal representation as well. That opens up some really interesting possibilities for objects to determine their own representation (string, HTMLElement, etc.)

I cheat here by using replace, but it gets the point across: https://codepen.io/kamiquasi/pen/NWwMBoR

Could you expound on this a bit? Are you saying that `yourObject.toTemplate('some template string') might pick and choose what properties are available to that template string, instead of making all of its properties into something that can be picked off and inserted into the string? So, different objects may provide different implementations?

In general, adding stuff to the Object prototype has been deemed off-the-table, due to the risk of causing issues with anyone else who has ever put a toTemplate function in an object literal. i.e. you can imagine existing code like this:

return someObj.toTemplate ?? someDefaultValue

Before, if someObj didn't have a toTemplate property, it would return someDefaultValue. Now, it will return the toTemplate function available on the object prototype, and someDefaultValue will never be returned.

What you can do instead, is make this function a static function on Object, then allow anyone to customize the behavior of this function by adding a symbol property to their object.

String.prototype.replace can also be passed a function, which gives a one-liner:

function render(template, state) {
  return template.replace(/\${\w+}/g, p => state[p.slice(2, -1)]);
}

render("hello ${name}", {name: "Alice"}); // "hello Alice"
1 Like

I'm definitely onboard with whatever the most sensible rollout would be to reduce collisions. toTemplate() just made sense in my head given the parallels to toString(). Maybe it would function a little like the interaction between JSON.stringify and toJSON a bit.

So this is what I would expect to be able to do (whatever the phrasing):

  • myObj.toTemplate("Hello ${prop}") - returns an untagged template literal referencing myObj.prop
  • myObj.toTemplate((str, ...props) => props) - returns a tag function such that I could actually use myObj.toTemplate to create a tagged template literal (this has some really interesting side-effects)
  • myObj.toTemplate( someTagFx `${otherObj.prop}` ) - returns a tagged template literal; I'd see this as passing the myObj scope into it, but not sure on this one.

I wasn’t able to follow exactly what this would do. I thought the initial problem was due to not being able to use tagged template literals?

I'm not sure I follow. The holes in template literals can be any JavaScript expression. In that sense, converting a string into a template strictly means evaluating the string within a given context, just like eval.

If the reason you can't include the template source in your code is because it's externally provided, I would recommend you don't blindly evaluate it.

If what you're looking for is to only allow a subset of expressions which are identifiers, then a templating library is more appropriate.

2 Likes

Perhaps could you provide a more concrete example of how you would use this toTemplate function in your own code? And, maybe restate why you want a toTemplate function (is it because you can't use template literals, like @aclaymore suspected? Or is it because you're loading a template string from an external source? Or is it something else?)

That's equivalent to eval (if '${(() => { while(true);}}()'.toTemplate() is allowed) or templating system (out of scope).

@Ginden I think you've hit on the thing I've avoided pondering. Given the generic parsing of a string as a template literal there would be risk of executing, potentially harmful, arbitrary code.

I probably need to just get behind the Template Instantiation proposal more. The pain point is really concerned with the existence of <template> and the inability to parse it fully built-in to the language.