Re: [WICG/webcomponents] [templates] A declarative JavaScript templating API (Issue #1069)

dariomannu left a comment (WICG/webcomponents#1069)

@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:
```html
<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.

> 2. 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:
```html
<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):

```javascript
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:

```javascript
// 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.

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

```javascript
  <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](https://lit.dev/docs/libraries/standalone-templates/) 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.

-- 
Reply to this email directly or view it on GitHub:
https://github.com/WICG/webcomponents/issues/1069#issuecomment-2831996920
You are receiving this because you are subscribed to this thread.

Message ID: <WICG/webcomponents/issues/1069/2831996920@github.com>

Received on Saturday, 26 April 2025 09:41:54 UTC