Skip to content

[templates] A declarative JavaScript templating API #1069

Open
@justinfagnani

Description

@justinfagnani
Contributor

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

changed the title [-][templates] A JavaScript templating API[/-] [+][templates] A declarative JavaScript templating API[/+] on Aug 17, 2024
EisenbergEffect

EisenbergEffect commented on Aug 17, 2024

@EisenbergEffect
Contributor

Immediate questions I have:

  • How does this integrate with signals?
  • Can fine-grained updates be accomplished or is re-rendering required?
  • How do we expect a server DSD version to be generated? How do we expect it to resume on the client?
  • Can we have an in-HTML equivalent that doesn't require JS? Pretty please.

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

justinfagnani commented on Aug 17, 2024

@justinfagnani
ContributorAuthor

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.:

const count = new Signal.State(0);

document.body.render(HTMLElement.html`<p>count: ${count}</p>`);

count.set(count.get() + 1);

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

justinfagnani commented on Aug 17, 2024

@justinfagnani
ContributorAuthor

How do we expect a server DSD version to be generated? How do we expect it to resume on the client?

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

justinfagnani commented on Aug 17, 2024

@justinfagnani
ContributorAuthor

Can we have an in-HTML equivalent that doesn't require JS? Pretty please.

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

JRJurman commented on Aug 17, 2024

@JRJurman

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.

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 parseHTMLUnsafe or setHTMLUnsafe. 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 called unsafeHTML).

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

justinfagnani commented on Aug 17, 2024

@justinfagnani
ContributorAuthor

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

rictic commented on Aug 17, 2024

@rictic
Contributor

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

NullVoxPopuli commented on Aug 18, 2024

@NullVoxPopuli

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:

const count = new Signal.State(0);

document.body.render(HTMLElement.html`<p>count: ${count}</p>`);

count.set(count.get() + 1);

count is rendered reactively, inherently -- which is great! This isn't where my concern is tho. Say you have a situation like this:

class Animal {
  givenName = new Signal.State();
} 

class Cat {
  get name() {
    return `${this.givenName}, the cat`;
  }
}

class Dog {
  name = () => {
    return `${this.givenName}, the dog`;
  }
}

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:

let cat = new Cat();
let dog = new Dog();

document.body.render(HTMLElement.html`
  <details><summary>Your Cat</summary>
    ${() => cat.name}
    If we don't specify a function, ${cat.name} is burned in and non-reactive, 
    as the value of name is eagerly evaluated.
    This would require that the processing of HTMLElement.html call functions, 
    if encountered.
  </details>
  
   <details><summary>Your Dog</summary>
    ${dog.name()}
    Not everyone is on board with getters when it comes to reactivity, and that's fine,
    but the other way of accessing *maybe* reactive values is a function,
    this also burns in the value, and would require non-invocation, e.g.: 
    ${dog.name}.

    This reveals in an inconsistency and footgun between:
      burned in:
         ${cat.name}
         ${dog.name()}
      reactive, provided HTMLEelment.html calls all functions:
         ${() => cat.name}
         ${dog.name}
  </details>
`);

// some time later
cat.givenName = 'Alexander';
dog.givenName = 'Sir"

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).

let cat = new Cat();
let dog = new Dog();

document.body.render(<template>
  <details><summary>Your Cat</summary>
    {{cat.name}}
  </details>
  
   <details><summary>Your Dog</summary>
    {{dog.name()}}
  </details>
</template>);

// some time later
cat.givenName = 'Alexander';
dog.givenName = 'Sir"

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

au5ton commented on Aug 18, 2024

@au5ton
class Cat {
  get name() {
    return `${this.givenName}, the cat`;
  }
}

class Dog {
  name() => {
    return `${this.givenName}, the dog`;
  }
}

@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

brunnerh commented on Aug 18, 2024

@brunnerh

Usage of Computed with signals is not strictly necessary.
The main advantage they provide is memoization of expensive calculations.

justinfagnani

justinfagnani commented on Aug 19, 2024

@justinfagnani
ContributorAuthor

@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

justinfagnani commented on Aug 19, 2024

@justinfagnani
ContributorAuthor

@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:

  1. At the DOM Part level: Individual parts accept signals and update when the signals change. To feed in a computation that uses signals, you'd first wrap it in a computed to tell the DOM Part to watch it.
  2. By wrapping a template expression + .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.
  3. An integration of (1) + (2) where a DOM Part holding a signal defers its updates to an external scheduler, so that they can use the same scheduler as in (2). This could let pin-point updates happen without a full template re-render, or be batched with a full re-render if one is already scheduled. This kind of deferring scheduling would also work with a component system that controls the effect and scheduler.

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

EisenbergEffect commented on Aug 19, 2024

@EisenbergEffect
Contributor

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

dariomannu commented on Jan 21, 2025

@dariomannu

@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:

// With Promises
const p = fetch(url).then(r=>r.json()).then(json=>json.stuff);
const template1 = `<div>${p}</div>`;


// With Observables
const stream = new Subject |> filter( ... ) |> map(...); // just a random pass-through observable stream
const template2 = `
  <button onclick="${stream}">click me</button>
  <div>${stream}</div>
  <div class="${stream}"> ... </div>
`;

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...

self-assigned this
on Mar 19, 2025
justinfagnani

justinfagnani commented on Mar 29, 2025

@justinfagnani
ContributorAuthor

@dariomannu

You know what I would add to it?
Support for Promises and Observables/Observers as template parameters

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:

  1. Where are different values handle? In the template layer or the DOM Parts layer?

I think values should just be passed through to the DOM Part created by the templates, with the possible exception of Directives (see 3).

  1. What's the criteria for support of a data type?

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.

  1. How do users add support for new data types?

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.

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.

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

vospascal commented on Mar 30, 2025

@vospascal

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 something

extending 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

justinfagnani commented on Mar 31, 2025

@justinfagnani
ContributorAuthor

@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

vospascal commented on Mar 31, 2025

@vospascal

@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?

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 it expressionFunction inside 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

trusktr commented on Apr 2, 2025

@trusktr
Contributor

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.

import {createEffect} from 'whatever-library'

createEffect(() => {
  const result = html`
    <p>value ${anyValue()}</p>
  `

  // ...
})

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:

  const result = html`
    <p>value ${anyValue()}</p>
  `

  console.log(result instanceof HTMLParagraphElement) // true

This,

document.body.render(renderPost('Hello World', 'This is a template'));

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 html tagged 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

trusktr commented on Apr 2, 2025

@trusktr
Contributor

You know what I would add to it?
Support for Promises and Observables/Observers as template parameters

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:

  1. Where are different values handle? In the template layer or the DOM Parts layer?

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.

  • People can wrap their templates in effects if they want to use signals.
  • They can call their own functions or methods to rerun templates when promises resolve.

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:

  • specify desired behavior of attribute, text content, and list interpolations
  • return the DOM (or DOM list) result from the html tag function, which is cached by source location.
  • the only new JS API for web devs is the html tagged template literal, with an optional way to key the cache:
    • f.e. 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:

<script>
  function myTmpl(a, b, key) {
    // undefined key is the same as not using a key
    return html(key)`<p>
      One value: ${a} <br />
      Other value: ${b}
    </p>`
  }

  const p = myTmpl(123, 456)
  
  document.body.append(p)

  // ... later update the DOM ...
  myTmpl('foo', 'bar')

  // need a new instance? Then:
  const sym = Symbol()
  const p2 = myTmpl('a', 'b', sym)
  document.body.append(p2)
  // ... later call tmpl2 when needed
  tmpl2('c', 'd', sym)
</script>

Done. That's it! This will be a smaller spec and we can do two things:

  • ship something sooner
  • have it be useful without requiring significant JS or frameworks.

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:

<script>
  function makeTmpl() {
    return new Function('a', 'b', `
      return html\`<p>
        One value: \${a} <br />
        Other value: \${b}
      </p>\`
    `)
  }

  const tmpl = makeTmpl()
  const p = tmpl(123, 456)
  
  document.body.append(p)

  // ... later update the DOM ...
  tmpl('foo', 'bar')

  // need a new instance? Then:
  const tmpl2 = makeTmpl()
  const p2 = tmpl2('a', 'b')
  document.body.append(p2)
  // ... later call tmpl2 when needed
  tmpl2('c', 'd')
</script>

Lastly, we could make the key option be keyable with the template's own return value:

const tmpl = (val, key) => html(key)`<p>value ${val}</p>`

const p = tmpl(123)

document.body.append(p)

// ...later, update the specific DOM:
tmpl(456, p)

What's the absolute most minimal smallest possible spec that is most useful for end web API users.

sashafirsov

sashafirsov commented on Apr 15, 2025

@sashafirsov

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:

  • attributes
  • data- attributes subset
  • form data
  • DCE payload DOM
  • data slices explicitly dedicated by DCE
  • event data

Note 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 attribute attr1. 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

dariomannu commented on Apr 26, 2025

@dariomannu

@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...

There are a fairly large number of data types that could be natively handled by the template system.

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:

<button onclick="${anObserver}">
<button onclick="${aFunction}">
<button onclick="${aSignal}">
  1. Where are different values handle? In the template layer or the DOM Parts layer?

I think values should just be passed through to the DOM Part created by the templates, with the possible exception of Directives (see 3).

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.

  1. What's the criteria for support of a data type?
    ...
    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.

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:

<div class="${aPromise}">
<button disabled="${anObservable}">
<div style="color: ${anObservable}; background: ${anotherObservable};">

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):

const template = `
    <div>${CustomScheduler( source1 )}</div>
    <div class="${OtherScheduler( source2 )}"> styled content </div>
`;

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:

// I've never done any of these, but might work as well:
const template = `
    <div scheduler-for-innerhtml="some-predefined-scheduler">${ source }</div>
    <div scheduler-for-class="${aCustomScheduler}" class="${ source }">the content</div>

    <base scheduler="whichever">
        <!-- everything here would run under the said scheduler -->
    </base>
`;

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.

  1. How do users add support for new data types?

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.

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?

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.

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)

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.

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:

  <div data-foo="${ aStream }">   a dataset sink
  <div style="${ aStream }">   a style attribute sink
  <div style="color: ${ aStream }">   a style value sink
  <div ...${ aStream }>   an attribute sink (to support declarative Attribute Mixins)
  <div>${ aStream }</div>   an InnerHTML sink
  <div>${ InnerText(aStream) }</div>   an InnerText sink, requested explicitly
  <div>${ aStream |> InnerText }</div>   an InnerText sink, requested explicitly if pipeline ops land

Cool! I love seeing more template literal based systems - it proves their suitability for this domain.

Perfectly suitable, indeed!

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.

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

dariomannu commented on Apr 27, 2025

@dariomannu

@trusktr

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.

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:

target.innerHTML = `<div class="${strClass}">${ arrayOfStrings.map(whatever).join('<br>') }</div>`;
  • People can wrap their templates in effects if they want to use signals.
  • They can call their own functions or methods to rerun templates when promises resolve.

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:

const data = fetch('/api').then(json).then(whatever);

target.innerHTML = html`
  <div>${data}</div>
`;

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!).

It's also very important to take the time to think stuff through, as there's no easy way back once it's done...

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.

Can't see that. If you have some good proposals, maybe on a more open platform like this?

// ... later call tmpl2 when needed
tmpl2('c', 'd', sym)
</script>
Done. That's it! This will be a smaller spec and we can do two things:

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.

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:

<script> function makeTmpl() { return new Function('a', 'b', ` return html\`

One value: \${a}
Other value: \${b}

\` `) }

And this is why I'm saying add support for Promises and Observables, so the world wouldn't have to do this.

vospascal

vospascal commented on Apr 28, 2025

@vospascal

The "move fast and break things" guys have already done their damage to the world leaving others to clean up the mess. If you're only looking for a paid retirement, there are other ways to get one without the same dishonourable mentions 🙃, so why don't we just take the time to think? Once it's done, there's no way back..

I please would like to remind you of the code of conduct even if you don’t agree with someone please be respectful

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @rictic@sorvell@keithamus@EisenbergEffect@littledan

      Issue actions

        [templates] A declarative JavaScript templating API · Issue #1069 · WICG/webcomponents