Description
Related to many template, template instantiation, and DOM parts discussions (but especially #777, #682, and #704) I wonder if we should add a JavaScript-based templating API?
I think JS-based templating would be extremely useful and help with a lot of issues and questions:
- Ergonomics and convenience: Templating in JS is an incredibly common thing to do on the web, yet there's no built-in API for it. Developers have often asked for a built-in declarative way to build and update DOM.
- Safety: We can provide a safe way of interpolating untrusted values into the DOM protecting against XSS.
- Clarify the MVP for DOM parts: Templating in JS can cover a lot of open questions for template instantiation, like those brought up in [templates] How featureful should the default processor be? #682, like what the expression syntax is, how to do conditionals and looping, how scoping works, etc.
- Performance: As shown by several JS libraries, the DOM Parts model can be used to construct very fast DOM renderers. Fast, stable DOM updates as a platform feature would be a performance feature.
- Improve the initial value delivered by DOM Parts: The DOM Parts model can be used for fast templating, but given some Chrome prototypes a native implementation might not be a large performance win over similar userland implementations. There is still a lot of value in offering built-in templating, however. As @rniwa said about template instantation it "provides a mechanism to easily create a template instance without having to import a third party library"
- Code size: It would also reduce code size for those that use the native feature, and possible higher-level libraries that build on top of it. The tagged template literal syntax can be a compile target for other popular template syntaxes like JSX, Mustache, and HTML-like template like Vue, Angular, Svelte, etc.
- Interoperability: If multiple user-facing template systems use this API, they could interop, ie render a JSX template into a tagged-template literal, a Mustache template into JSX, etc.
Idea
Add an HTMLElement.html template tag and an HTMLElement.render() method that would allow describing the desired DOM structure in JS, along with bound data for interpolation and updating.
Templates would be written as tagged template literals using the HTMLElement.html tag:
const {html} = HTMLElement;
const renderPost = (title, summary) = html`
<h1>${title}</h1>
<p>${summary}</p>
`;Templates would be rendered to an element with the HTMLElement.render() method:
document.body.render(renderPost('Hello World', 'This is a template'));DOM rendered from a template can be updated by re-rendering with the same template, preserving the static DOM and only updating the bindings that changed. This would be done with DOM Parts.
// Only updates the <h1> element created from the template. No other DOM updates.
document.body.render(renderPost('Hello Templates', 'This is a template'));Features
Detailed features can be worked out later, but we know there are some basics that need to be covered to make a useful DOM templating system:
- Various binding types: child, attribute, property, event listener, element.
- Auto-escaping of binding values
- Fast updates with minimal DOM mutations: re-rendering a template should only update bindings that change.
- Composition: the ability to render a child template or array of child templates into a child binding
- Extensibility: Many use cases require some customization of how a binding is processed
- Hydration: There needs to be a way to update DOM that has been pre-rendered with SSR, SSG, etc.
Prior art
The general shape of this API has been popularized by several userland libraries including hyperHTML, lit-html, and FAST.
Activity
[-][templates] A JavaScript templating API[/-][+][templates] A declarative JavaScript templating API[/+]EisenbergEffect commentedon Aug 17, 2024
Immediate questions I have:
These days, I'm highly focused on declarative patterns that don't require JS for initial render. I know we want to move the ball forward for templating, but I'd still prefer to move it forward on the HTML side rather than in JS. For most of the clients I'm consulting with, this will not help their key scenarios at all. More progress on declarative approaches is what they need.
Note that in FAST, things work a good bit differently, even though the tagged template literal part looks the same. In FAST, state is backed by signals and bindings are live. So, there is no re-render step. Rather, when a signal value changes, FAST makes a fine-grained update to precisely the DOM node that has changed (batched with any other changes that happened in the same micro task).
justinfagnani commentedon Aug 17, 2024
I was just about to add a note about signals. I think this is a great place to start adding signals integration to the DOM.
If you use a signal in a binding, that binding should be bale to update without a full re-render.:
But not every bit of state in a page is or ever will be signals, so I think the ability to have a fast re-render is necessary.
justinfagnani commentedon Aug 17, 2024
I think the key there is defining the hydration protocol - which is very closely related to SSR'ed DOM Parts. Once you have an HTML syntax that can be hydrated, then servers can generate that HTML. JavaScript servers can interpret the template literals to do so.
justinfagnani commentedon Aug 17, 2024
I just think that this is a lot harder to do with all the features that real world templates require. The nice thing about starting in JS for full-feature templates is that the underlying DOM Parts / Template Instantiation layer can be much simpler.
Otherwise will have to figure out expression syntax and semantics, scopes, control flow, within the context of HTML up front. With a JS layer, JavaScript takes care a lot of that for us. I think then we could continue with the HTML syntax.
JRJurman commentedon Aug 17, 2024
Agreed on this point. While I think this is common enough that it probably is worth baking into the platform, I do feel like I'd rather see us make strides to do HTML building in HTML.
On a separate point, I wonder how much we would want to lean on existing Template parsing functions, like
parseHTMLUnsafeorsetHTMLUnsafe. While potentially not as feature rich as we might want, it probably isn't hard or terribly contentious to bake in a cleaner interface for these functions (even if that means making it calledunsafeHTML).If this should be very different from those functions, it might be worth calling out in the beginning of the proposal, and in what ways. I worry about the number of ways we have to create DOM from string templates, and it's worth calling out how these might be different (rather than adding to an existing one).
justinfagnani commentedon Aug 17, 2024
While having the full initial HTML for a page is great, the DOM includes a lot of imperative, dynamic mutation APIs and is still sorely missing a safe, fast, declarative DOM creation/update API.
This is a natural compliment and alternative to
innerHTML,createElement(), etc. And it does not preclude features to make server-rendered HTML more useful.rictic commentedon Aug 17, 2024
I know that Google's internal security team has replaced a bunch of innerHTML calls in our internal codebase with Lit to improve security. Putting this in the platform seems like a solid thing to point people to when they'd otherwise reach for innerHTML, would greatly improve security and (for updates) performance too.
NullVoxPopuli commentedon Aug 18, 2024
I have a concern, but I'm not sure where it belongs -- but with reactive rendering w/ Signals -- and maybe this is something that a library/framework would use to solve my concern, but -- given this example:
countis rendered reactively, inherently -- which is great! This isn't where my concern is tho. Say you have a situation like this:Assuming we use the same rendering function from the example, we can't reliably or consistently render reactive data, unless we pass on the responsibility to handle all situations to the developer:
Now, what I would prefer, is taking a templating system further, and not using tagged template literals at all -- (though, I don't immediately see a reason why what I'm about to show couldn't be implemented with the above).
In this example, both ways you'd access the value in JS would inherently be reactive -- there is no need to know about any footguns, because there would be none (unless someone points out something to me haha). This removes the need to worry about what is reactive and what is not, no need to
${() => value}everything due to not knowing if something could be reactive (A library may always be able to make something reactive in the future, and you wouldn't know!)This uses https://wycats.github.io/polaris-sketchwork/content-tag.html which is very pre-proposal, but is a variant on something that the corner of the JS ecosystem I'm in has decided to use for its rendering.
(But again, I guess it could be built on tagged-template-literals, and be more of a sugar)
anywho, my 2c
au5ton commentedon Aug 18, 2024
@NullVoxPopuli From what I understand, this would be an incorrect usage of Signals. I believe you're supposed to use
new Signal.Computed(...)for such a scenario.See: https://github.com/tc39/proposal-signals?tab=readme-ov-file#example---a-signals-counter
brunnerh commentedon Aug 18, 2024
Usage of
Computedwith signals is not strictly necessary.The main advantage they provide is memoization of expensive calculations.
justinfagnani commentedon Aug 19, 2024
@o-t-w JSX would require major changes to JavaScript via TC39. A tagged template literal based system wouldn't.
But we've seen compilers that transform JSX to tagged template literals before, so frameworks that use JSX could target this API, or if JavaScript ever does add a JSX-like syntax, it could be made to work with this template system.
justinfagnani commentedon Aug 19, 2024
@NullVoxPopuli if there's any appetite for this idea, we can go into a lot of detail about how signals integration would work.
At a high level for now, I think that there are a few good spots for integration:
.render()call in an effect so that any signal access is tracked and updates cause a re-render. By doing the effect outside/around of the.render()call, it lets the effect setup use the scheduling it needs - microtask, framework scheduler, etc.An API to pass in the external scheduler could take the form of extra options to
.render(), though the shape would have to be figured out - is the option a schedule callback, or a Watcher, or is there an event?We do something similar to this with lit-html where a RenderOptions (maybe it would be better named a RenderContext though) object is passed down the tree of templates as they are rendered.
Re the second syntax you show, I'm trying to propose something here that doesn't require any changes to JavaScript, but could use them if they ever happen in the future.
EisenbergEffect commentedon Aug 19, 2024
Getting back to the issue that @NullVoxPopuli raises...what if we could introduce the idea of lazy evaluation to tagged template literals? For example,
${value}is the same eager evaluation that we have today, but{value}could be lazily evaluated. Essentially, it would get converted to something like${() => value}under the hood. It's maybe too late to change tagged template literals, as that would be a break, but maybe a new literal type with three backticks or similar could enable this. Or what if the tagged template function itself could opt into this interpretation somehow?Obviously, this then becomes a TC39 thing, which complicates matters. But it seems to me that tagged template literals could be a lot more powerful if that part of the language were explored more.
14 remaining items
dariomannu commentedon Jan 21, 2025
@justinfagnani, turning this concept into a standards proposal is epic!
You know what I would add to it?
Support for Promises and Observables/Observers as template parameters, as in:
Anyway, I might have something for you here.
I'm behind rimmel.js, probably the closest working "userland" match you can find to this proposal, except it directly challenges Signals and promotes an improved use of Observables instead.
If you want to create working examples of your concepts and ideas, ping me or get some inspo from this Stackblitz, too. It's also starting to support web components in the same style...
justinfagnani commentedon Mar 29, 2025
@dariomannu
There are a fairly large number of data types that could be natively handled by the template system. I think the important questions about this are:
I think values should just be passed through to the DOM Part created by the templates, with the possible exception of Directives (see 3).
In my experience with userland libraries of this concept, you want to keep the number of data types you support fairly small and close to what the DOM already accepts. One reason is code size, complexity and bugs - fewer data types is less of all. Another is having standard semantics and familiarity to the existing DOM.
The important exceptions have been primitives, iterables, and nested templates. These are needed for very basic control flow and binding types.
Being native changes the code size and familiarity considerations, but not necessarily complexity and bugs.
One criteria I think I think is important is to not natively support asynchronous data types unless there is a story for scheduling and batching their updates. I do think there should be a story for that though, and that async types like Signals, Observables, and Promises should enqueue a batch update so that the DOM is coherent.
The native system can't handle all useful data type, and it can't handle non-standard userland interfaces. And a good system for adapting new data types into templates takes pressure for the template system to accept too many data types.
One way to do this is to have one primitive types that's a good target to adapt other values to. Signals might be a good candidate here. Any async value-producing type can be piped to a Signal and update its value, and the DOM Part can respond to that update.
Directives are another way. A Directive is a stateful object that has direct access to the underlying DOM Part. Directives essentially customize expression value handling in a very generic way. A Directive can subscribe to a container value, such as a Promise, Observable, Stream, Async Iterable, etc. and write new values to the DOM Part. Directives can also get access to the DOM controlled by a DOM part, so they can do other complex operations like moving DOM.
Cool! I love seeing more template literal based systems - it proves their suitability for this domain.
I think that currently, lit-html is the closest thing to a working prototype of this idea. It was designed after the initial proposal and discussion of Template Instantiation and internally has many of the same concepts, including DOM Parts. lit-html was designed as if it were an API that the DOM could realistically have natively, so it tries to hew very closely to DOM concepts and data types.
vospascal commentedon Mar 30, 2025
i was more thinking cant you extend the template literal bindings
tagFunction`string text ${expression} string text`this is what is currently but what if you could apply a tagFunction like thing on the expression it self$expressionFunction{expression}people could do things like$dom{expression}$${expression}make there own bindings there expression could keep the reference alive since we already have this. i know this is only js side of things.there also could be an default for example
$${expression}where you could just set the value of the expression bit like a proxy or somethingextending template tag to work with expressions would also be an idea but not sure how i would focus on js part first
since the template literal and expression syntax are familiar for most developers
justinfagnani commentedon Mar 31, 2025
@vospascal I definitely would not propose changing template literals at this point. This would be purely a DOM API, and not require any changes to JS.
But what could
$expressionFunction{expression}do that${expressionFunction(expression)}can't?vospascal commentedon Mar 31, 2025
well i
${expressionFunction(expression)}is more inside the expression side of things and i feel is always evaluates/computes.if you would do
$expressionFunction{expression}you might be able to do some pre evaluation/computation things like potentialy life cycles or something i dont think that would make sense if you put itexpressionFunctioninside the{}also the template tag suggestion of having a binding syntax like
{{}}in my opinion would be nice if it would match the template literal expression syntax so${}.trusktr commentedon Apr 2, 2025
This does simplify DOM templating in JS, but doesn't solve non-JS needs.
The Template Instantiation proposal is too complex, and I wrote about an alternative simpler approach here,
https://discord.com/channels/767813449048260658/1354736298182705212
namely that the requirements for templating features can be specified, with a zero-JS required by end users, and implementation details left to browsers.
On the topic of signals, the tagged templates could remain simple without caring about the existence of Signals, and instead a separate API can be used for that.
I don't think signal reactivity needs to be coupled to them.
Also while we're at it, I would want it to return actual DOM, and let the update mechanism be an implementation detail of the browsers because as a user of Web APIs I do not want to install frameworks just to use web features, that's just not great:
This,
is far too complicated of an API for what web devs need (imagine a hundred interpolation sites), requiring them to write more JS instead of less, or to import frameworks, which I don't like as a direction for APIs that I want in browsers as an end user.
I feel like we're designing all new APIs as libraries only for frameworks, which makes web APIs very non-ideal for non-framework authors. Ideally we eliminate frameworks with new web APIs rather than making frameworks required.
As an end user of Web APIs, I want the best DX out of the box. It would be great to work on specs from that direction, then add things for framework authors only if needed.
As a user, if I have to use a framework, then it literally makes no difference if I use any
htmltagged template literal vs one that ships with a browser, therefore eliminating a lot of the value that shipping one in a browser could add.trusktr commentedon Apr 2, 2025
With these questions we're potentially asking for more complexity and maybe it's too much to spec.
We don't need to support promises, signals, or anything else but plain values for an initial spec.
We don't need to decide how DOM Parts can handle various types of values, and we don't even need DOM Parts.
We want to ship something usable to users ASAP before they've all left the web and done something else with their lives and we've all become old farts (myself included!).
Most minimal spec
The simplest possible solution is to do with this spec something similar as I've described for simplifying the Template Instantiation proposal in my previous comment, and leave the DOM-updating implementation details to browsers, with the most minimal API exposed to users.
The most minimal API spec for this tagged template proposal is:
htmltagged template literal, with an optional way to key the cache:html(key)`...`so that it can be keyed on custom criteria besides source location, such as per custom element instance, etc.This would be a very small API surface area that browsers can implement the details of in whichever most optimal way they see fit, very easy to spec without all the other details, and would require no frameworks and the most minimal JS requirements for end users.
Example
Minimal example inside an HTML file:
Done. That's it! This will be a smaller spec and we can do two things:
Note, even without a key option, there's still a way to bypass source location caching. Here's the previous example without using the key option, though not as ergonomic for end web API users to write:
Lastly, we could make the key option be keyable with the template's own return value:
What's the absolute most minimal smallest possible spec that is most useful for end web API users.
sashafirsov commentedon Apr 15, 2025
I disagree on the primitive treatment of data and their binding.
The data should reflect the scope where template is used. DCE ( declarative custom element ) for example needs access to:
data-attributes subsetNote that all of above fits into generic DOM. Just as data, not a UI. It already has XPath implementation. WIth default point to attributes the expression
{attr1}is evaluated from attributeattr1. XSLT has all of that.By keeping the primitive sygnal binding we are limiting the scope of the data. On another side, it is up to vendor to deside the mechanizm. The data DOM can be virtual and never materialize until used or at all like in case with attributes as the node value which matches the attribute would be same as the attribute in HTML DOM.
Concluding, there is no need to define on standards level the mechanizm, rather show the capabilities. Which are:
template+data DOM => rendered HTML
dariomannu commentedon Apr 26, 2025
@justinfagnani
This thread deserves to be a project of its own. I'm absolutely thrilled by this proposal but there are so many thoughts, ideas, views, notes and perspectives compressed together that's getting very hard to follow each point and get into the details...
Ok, first, let's split them in two categories: static values and reactive primitives, so we know what we're talking about, etc.
Values are static, should just be baked into the HTML and forgotten about, as they are not meant to be updated.
Reactive primitives can be split into push and pull-based streams.
Push-based streams: Promise, Observable.
Pull-based streams: Iterator, AsyncIterator.
It's also important to distinguish in nomenclature where the data comes from (event sources) and where it goes (data sinks).
I had a hard time reading this thread to figure out a rigorous definition of Template Parts vs DOM Parts, so just went with what a more clever LLM told me, but it would be good if we could have some formal definition for these.
To make the use of these templates intuitive, the context in which template expressions are used should be used to determine the required binding. E.g.:
<div class="${aPromise}">should set its class when the promise resolves, whilst<input value="{anObservable}">should set the value every time the observable emits.I belive this is what you mean by "the template layer should handle", but please correct me if I'm wrong.
On the other side, event sources should feed the given Observer or function provided:
Can you add a few examples, please, to make sure it's clear what you mean?
I've seen higher-up some references to the concept of "re-rendering", where if you re-assign a template to a DOM node, it should do a diff+patch step. I believe this is unnecessary and the sole use of the above reactive primitives will be sufficient to implement any type of high-performing fine-grained reactivity without the added complexity of a VDOM.
A VDOM or anything that requires it should probably not be part of this proposal.
Natively supporting async data? That's actually one, if not the single best feature of reactive templates, I would argue.
It enables us to do:
Batching updates, in my experience, is in the vast majority of cases unnecessary, so it shouldn't be the default.
Think of a simple button click in an app: some data is processed and a result is displayed. Pretty simple. If it's async data, it just renders when ready. No need for batching.
Think of some basic interactivity, such drag'n'drop. Doing sync drags appeals better than adding even a single-frame delay caused by batching. It may be unnoticeable, but it's not needed by default, let alone the extra weight batching adds, even if it's small, it's not needed here.
The remaining cases can suddenly have very advanced and specific requirements. Some sort of batching there may be useful or necessary, or just a game-changer for performance and that's fine, but should the browser provide the batching scheduling logic and implementation or should apps be able to provide their own, or a bit of both?
In Rimmel we're experimenting sink specifiers that enable you choose a particular scheduler within the template (so another aspect that could be handled by the templates layer):
So with the above you could have exact control over which reactive streams should run on a scheduler.
Actualy, this is something that could be delegated to the DOM parts, as well, with the use of attributes:
Schedulers could consume any type of stream, push or pull based, thus opening a multitude of possibilities.
Use cases for custom schedulers that devs can control? Busy UIs, where they want to prioritise different classes of actions over others as simply as just using a single scheduler attribute.
Anyway, schedulers are an interesting and complex topic, but may also fit for a second version of a proposal. I'd say they are optional and not a priority, as a number of reactive primitives already have a great scheduler support (see RxJS) that can be used already.
I think it would be enough to support at least the Promise, Observable/Observer interfaces for push-based reactivity.
Push-based reactivity can cover all cases (some people haven't realised this yet), but I appreciate there's a lot of pressure to get pull-based reactivity in, which could be covered by Iterable and AsyncIterable if you also supply a scheduler (or set a default one). I believe any other format could be wrapped in any of these, which are just interfaces, at the end of the day.
I don't believe there is a case for more advanced models such as mixed push+pull combos, agree?
Promise and Observable/Observer can handle all cases of reactivity and are arguably the simplest to support (Rimmel proved it, it's really simple to make these work). Reason being all complexities like flow control, etc, are already handled by those primitives or related libraries.
Iterable/AsyncIterable is something I only started contemplating recently. They sound interesting, wouldn't say essential, but they need schedulers to know when to pull the next value, so a bit of extra complexity, still.
Signals are overhyped and their effect system more complicated than it should, but if they exposed a subscribable interface like Observables do, adding support to them would become just as simple (Someone might disagree, so if in doubt, happy to expand on the reason why)
I like to refer to these as "sinks", but... whatever.
By saying "generic" what exactly do you mean, they can handle many types of operations, like changing innerHTML and also classes, dataset, events?
Not sure I'd agree on them having to be generic, though, as if they are generic, they become at least complex and a bit slower (having to handle different types at runtime). Specific ones can perform their specific functions in highly optimised ways. I guess we're referring to specialised directives/sinks as:
Perfectly suitable, indeed!
Ok, I see the connection now, makes sense.
Rimmel was originally inspired by Lit and htm with regards to using template literals, but it then grew on the functional/streams oriented path.
With regards to this proposal, I think a few picks from its functional take can bring some value here, as it shows how templating is perfectly viable without a VDOM, re-rendering, change detection, refs, keying, imperative component setup, JS classes and a ton of boilerplate.
dariomannu commentedon Apr 27, 2025
@trusktr
I would challenge the point of a reactive templates proposal if you don't even support reactive primitives.
If all you care about is plain values, untagged template literals rock:
I'm not looking to use signals. That's the whole point of binding reactive primitives like Promises and Observables.
If you do that, you'll no longer have to rerun, rerender components or even worse, wrap them in effects or other unnecessary boilerplate.
You shouldn't have to write any more than the below to render a Promise:
It's also very important to take the time to think stuff through, as there's no easy way back once it's done...
Can't see that. If you have some good proposals, maybe on a more open platform like this?
This is a bit far from being like the universal solution to all problems reactive primitives solve.
Understanding how those work may help to appreciate the value they can bring here.
And this is why I'm saying add support for Promises and Observables, so the world wouldn't have to do this.
vospascal commentedon Apr 28, 2025
I please would like to remind you of the code of conduct even if you don’t agree with someone please be respectful