Open justinfagnani opened 3 months ago
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).
I was just 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.
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.
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.
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).
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.
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.
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
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
Usage of Computed
with signals is not strictly necessary.
The main advantage they provide is memoization of expensive calculations.
JSX syntax has proven to be a lot more popular among developers than template literal ${} syntax. The extra $ character is unfortunate imo.
How would this potentially relate to https://github.com/WICG/webcomponents/issues/1059
@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.
@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.
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.
@EisenbergEffect I would hope that any JS template API could take advantage of future changes to JavaScript.
If there is some future tagged template literal syntax that allows deferred evaluation of interpolations, I would presume that the tag function would be able to detect that, and we could feed that bit into the return type of the html
tag, and into .render()
so that it could treat the interpolated values differently.
@NullVoxPopuli
there is no need to know about any footguns, because there would be none (unless someone points out something to me haha).
Do I have this right? Since what you propose would rely on the proposed JS content tag, it would necessarily involve a compiler and that compiler would be responsible for doing something like wrapping all the signal access in functions, or whatever is needed to ensure the update can be fine-grained.
If so, would you agree the downsides are something like: (1) requires a build step, (2) depends on a robust potentially complex build compiler?
@EisenbergEffect
what if we could introduce the idea of lazy evaluation to tagged template literals?
I've separately thought about this and it seems like a great thing to propose. It's a feature that can easily seem trivial in isolation: how hard is ()=>
? and (minus the obscurity) could you make anything more terse?
It's specifically when used with an API like this where you might need these arcane characters dozens of times that it becomes really onerous, as indeed even the $
at the front of values in literals is.
Hello everyone, I have some (most likely) naive questions:
const count = new Signal.State(0);
document.body.render(HTMLElement.html`
<div>
<p>You clicked ${count} times</p>
<button onClick=${() => count.set(count.get() + 1)}>
Click me
</button>
</div>
`);
@sorvell
it would necessarily involve a compiler and that compiler would be responsible for doing something like wrapping all the signal access in functions, or whatever is needed to ensure the update can be fine-grained.
yup -- this would have to be a manual step with the proposed html-template-literal function (else you toss fine-grained reactivity and re-generate the whole template each signal-change)
If so, would you agree the downsides are something like: (1) requires a build step, (2) depends on a robust potentially complex build compiler?
1) nay -- or rather, interpretation of the string-template can happen at runtime, much like template-literal functions 2) nay, see above
It's a feature that can easily seem trivial in isolation: how hard is ()=>?
Having a way for template-literals to implicitly imply () =>
could work, but I think would be a surprise, since ${}
already has semantic meaning. -- and then how would you ever do anything statically? (like today's default)
@mdesantis
- How would this integrate with events?
Underlying all template bindings will be Parts from the DOM Parts proposal. We will need a syntax for attribute-like bindings that distinguishes between attribute, property, and event parts. That syntax should be part of the static strings of the template so that one part can be created and use for that binding for the lifetime of the template instance. So... some kind of prefix or sigil is mostly likely needed.
In lit-html we use .
, @
, and ?
prefixes to denote property, event, and boolean attribute bindings. An event binding looks like:
HTMLElement.html`
<button @click=${(e) => { console.log('click'); }>
Click me
</button>
`
Now, I don't want to presume or propose a lit-html-like syntax at this moment, and I'm not sure if there'd be consensus on diverging from plain HTML like this, but there are some reasons why we chose these prefixes:
@
is a pretty common prefix for events (used in Vue and others), while .
looks like property access..
, @
, and ?
prefixes are parsable by the HTML parser, making it easy to generate template HTML from the template strings. This might not be a concern for a native implementation..
, @
, and ?
are not valid attribute names for setAttribute()
, making them less likely to be used in any existing HTML, so collisions with real attribute names are unlikely.I think the ergonomics work really well, and this setup has been copied by other libraries, so it seems like a good choice so far. Any standards proposal should consider all the options though.
Would something like this work?
const count = new Signal.State(0); document.body.render(HTMLElement.html` <div> <p>You clicked ${count} times</p> <button onClick=${() => count.set(count.get() + 1)}> Click me </button> </div> `);
The problem with the React approach of just using property names for events are that 1) not every element has an event-handler property for every event that you might want to listen to. Bubbling and capturing really dispel that notion. 2) There can be collisions with non-event properties that happen to start with on
. And 3) event-handler properties do not accept event options, while addEventListener()
does and an EventPart should.
I think an explicit syntax is much, much better.
- How would conditionals look?
- How would loops look?
Conditionals and loops are one of the big reasons to do a template system in JavaScript before doing one in HTML, IMO. They just fall out naturally from a few things:
With composition and template-as-values, you get conditionals:
const {html} = HTMLElement;
html`
<div>
${user.isLoggedIn
? html`<p>Welcome, ${user.name}</p>`
: html`<p>Please log in</p>`
}
</div>
`;
Support for iterables gives you looping:
const {html} = HTMLElement;
html`
<ul>
${items.map((item) => html`<li>${item.name}</li>`)}
</ul>
`;
Template-as-values also means that you can use any conditional or looping constructs that you want. You can use for-loops and push template results into an array. You can make custom conditional and iteration helpers. You can use generators, Sets, Maps, and any other iterable implementation.
Keying and moving DOM when data is reordered and mutated is an important concern. This can be built in userland on top of stateful directives. A directive can store the previously rendered items, perform a list-diff on update, and manually move DOM around to preserve state as needed.
To the use this would look like:
import {keyedRepeat} from 'dom-template-utils';
const {html} = HTMLElement;
html`
<ul>
${keyedRepeat(items, (item) => item.id, (item) => html`<li>${item.name}</li>`)}
</ul>
`;
Where the second argument to keyedRepeat
is a key function.
@sorvell
@EisenbergEffect
what if we could introduce the idea of lazy evaluation to tagged template literals?
I've separately thought about this and it seems like a great thing to propose. It's a feature that can easily seem trivial in isolation: how hard is
()=>
? and (minus the obscurity) could you make anything more terse?It's specifically when used with an API like this where you might need these arcane characters dozens of times that it becomes really onerous, as indeed even the
$
at the front of values in literals is.
It's not just about character count to me, but also about author intent. () => ...
could be a computed value, but it also could be just a function that you want to pass. If you default to interpreting it as computed value, then you need to do something like () => () => ...
to pass a function.
A template system also doesn't know if every binding should be interpreted as a computed signal or not, and so whether to watch it, so without either a manual approach or syntax, the template system has to watch everything.
That's why I think that one of the main benefits of a special is just capturing the user intent that a binding is a signal.
I agree with others' concerns that signals + template literals make a poor fit with current semantics, because signals naturally want expressions to be evaluated separately and potentially multiple times.
@littledan I actually think that tagged template literals are a great fit with signals because tagged template literals are so cheap to reevaluate. The handler of the template literal result can iterate through the values and also cheaply check which have changed.
There was a question at the TPAC breakout about the process of turning a JS template expression into a <template>
with DOM Parts.
The transform (a bit simplified) breaks down like this:
First, we have the html
tag implementation which captures its arguments:
function html(strings, ...values) {
return {kind: 'html', strings, values};
}
Then we have a function that returns a template expression:
const renderUser = (user) => html`<span>${user.firstName}, ${user.lastName}</span>`;
When called:
renderUser({firstName: 'John', lastName: 'Doe'});
it will return an object like:
{
kind: 'html',
strings: ['<span>', ', ', '</span>'],
values: ['John', 'Doe'],
}
With the object we need to be able to either perform an initial render to a new place in the DOM, or update an existing fragment of DOM already created from it. We need a template that's able to be interpolated with any values, not just the values from the initial render.
So we need a template with parts where the values would go, but that doesn't use the values. The only strings we have are ['<span>', ', ', '</span>']
. This array has one more item than the values array. The values will go in between the string spans we have, so to create the template HTML with placeholders for the values, we can join the strings with the DOM Parts marker ({{}}
):
const templateHtml = templateResult.strings.join('{{}}');
which gives us:
<span>{{}}, {{}}</span>
There's nothing inside the expression markers, like a name, because we don't have anything we can put there. The html
template tag doesn't get the text of the expressions in the template, only the evaluated values. We can't put those in the template because they're valid only for one particular render (and it's not safe to put them in the template HTML).
So then we can make a template:
const template = document.createElement('template');
template.setAttribute('parseparts', '');
template.innerHTML = templateHtml;
and to render it we clone it with parts, append the clone, and set the value of the parts.
const {node, parts} = template.cloneWithParts();
container.append(node);
parts.forEach((part, i) => part.setValue(templateResult.values[i]));
I hope that explains why the DOM Part expressions are empty ({{}}
). There is no name or identifier that we have to put there other than their index in the values array, which they already have implicitly by order.
And this is one reason why the JS API is simpler to do than the HTML syntax with expressions, because we don't need to specify any new expression language, or even identifier within the expression markers. The JS API doesn't need any of that to get a lot of utility from the basic DOM Parts delimiter syntax.
cc @LeaVerou @annevk
I'm breaking my coment into a few sections to make it easy to reply to whichever part you're interested in:
Now, I don't want to presume or propose a lit-html-like syntax at this moment, and I'm not sure if there'd be consensus on diverging from plain HTML like this, but there are some reasons why we chose these prefixes:
I vote in favor of it. I'm a fan of the syntax for the reasons you mentioned, and a native implementation will get around the case-sensitivity issue, 👍.
@titoBouzout and I have been ideating new semantics for Solid's templating (not guaranteeing anything here, it depends on final choices from Ryan), but we've so far been using attr:
, prop:
, bool:
, on:
in JSX, and for html
template tag we're contemplating moving to foo
, .foo
, ?foo
, and @foo
similar to Lit, and this will open the door to Lit tooling for type checking and intellisense that can also work for non-Lit html
(see the problem of function wrapper types in the TypeScript section below).
Plus, because Lit's html
has the most tooling built for it already (for type checking and intellisense), choosing the same syntax will make migration for such tools easier.
The only real problem I would change in Lit html
is that .foo="bar"
sets the ".foo"
attribute whereas .foo=${"bar"}
sets the "foo"
JS property, which I think is confusing. I would expect .foo="bar"
to also set the JS property. And as you mentioned @justinfagnani, it is unlikely anyone is relying on attributes named .foo
/@foo
/?foo
, so better to just change this one semantic. I've been bitten by it a few times when it silently fails to do what I want. Other than that, the fact that what-we-write-is-exactly-what-we-get with Lit html
, apart from that discrepancy, is really great because it does not block anyone from doing anything they need to do with the DOM without unexpected behavior.
In contrast, React 19's new Custom Elements support will have lots of unexpected behavior. One one custom elements you may write foo={123}
and also write the same thing on another element, but it might set the attribute on one, and it might set the attribute on the other. In my opinion not only is that potentially confusing, but it will also potentially block people from doing exactly what they want with the DOM.
Given this
const {html} = HTMLElement html`<ul> ${items.map((item) => html`<li>${item.name}</li>`)} </ul>`
How does this work with signals?
const {html} = HTMLElement html`<ul> ${items.get().map((item) => html`<li>${item.name}</li>`)} </ul>`
Indeed it seems only with {expr}
syntax as shortcut for ${() => expr}
.
But a question is, what if Signals
came out first?
Would it be plausible to make a rule that a function value will always be unwrapped in that case, so if we wanted to actually set a function value, we'd need to wrap it: .someFunction=${() => theFunction}
? Otherwise .someFunction=${theFunction}
would get unwrapped in an effect every time and would therefore not provide the function value?
The rule would be that function values always get unwrapped in an effect, which could be a rule that we would only be able to make if Signals
were already native to the language before html
came out.
But I'm not sure that's ideal...
Plus it, unless Signals
come to the JS language first before html
, it seems that new {expr}
syntax is the only way to avoid the issue that ${() => expr}
means the expression is a function in the eyes of TypeScript, rather than the type of the value we want to set.
F.e. .someNumber={value()}
where value returns a number would work, types would check out, but .someNumber=${value}
would cause tooling to see the value as a function type.
In order to make it work, we'd have to agree that all values need to be passed as function wrappers so that we can use ReturnType
to reliably determine if () => number
works when .someNumber
expects a number value.
People might complain about the ergonomics. So maybe {}
is indeed the solution.
html
return valueWhat I would really really want, is to be able to easily access the DOM in one go, making html
more convenient for app authors (typical web devs):
const signal = new Signal(0)
const div = html`<div>value: {signal.get()}</div>`
// Do anything else with DOM APIs
console.log(div instanceof HTMLDivElement) // true
div.append(someLibraryThatReturnsAnElement())
That's more convenient than if html
were to return something like [strings, values]
because returning [strings, values]
makes html
cumbersome to use without further having to process that into actual DOM.
If the browser engine can hide it, I don't see any reason not to.
But if we agree on DOM Parts
API, would we return those too?
const signal = new Signal(0)
const [div, parts] = html`<div>value: {signal.get()}</div>`
console.log(div instanceof HTMLDivElement) // true
Maybe parts
in that return value is an array of Parts in same order as template interpolations. Re-running a template would update the parts values I imagine. No need for values
array because we have the parts
already, right?
A more friendly return value like that seems more pleasant to work with for an app author, the typical web dev. App authors would be able to easily make a template, and to easily use the DOM result without any further dance.
It is simpler than having to think about all of this as an app author wanting write app code:
it will return an object like:
{ kind: 'html', strings: ['<span>', ', ', '</span>'], values: ['John', 'Doe'], }
So then we can make a template:
const templateHtml = templateResult.strings.join('{{}}'); const template = document.createElement('template'); template.setAttribute('parseparts', ''); template.innerHTML = templateHtml;
and to render it we clone it with parts, append the clone, and set the value of the parts.
const {node, parts} = template.cloneWithParts(); container.append(node); parts.forEach((part, i) => part.setValue(templateResult.values[i]));
With the simpler return value idea, the parts can still be available, so that library authors can still hook data into the DOM parts in other ways.
But do we even need parts? Can the browser just return the DOM result, and simply update that DOM whenever the template re-runs or signals change? Why do we need more than that?
But then there's another problem: different libraries have their own Context API that relies on templates executing inside of their reactive context. So this will not work in a library like Solid:
function SomeSolidComponent() {
const {node: div} = html`<div>
<${ComponentThatConsumesTheProvider} />
</div>`
return html`
<${ContextProvider} value=${123}>
${div}
</${ContextProvider}>
`
}
This will not work because the template that created the div
ran outside of the context of the other html
template, therefore the ComponentThatConsumesTheProvider
will receive a default value, and will never receive the value 123
that was passed to ContextProvider
.
The context API would only work if the template was written as follows unless Solid implements some sort of more difficult DOM traversal algorithm and stores references on DOM nodes to be able to traverse upward to find contexts:
function SomeSolidComponent() {
return html`
<${ContextProvider} value=${123}>
<div>
<${ComponentThatConsumesTheProvider} />
</div>
</${ContextProvider}>
`
}
But this implies that html
would support:
Is this even desirable? Seems like to keep things simple, we'd just support HTML elements, no function components (because function components are something from framework land that are not part of DOM).
People would write only Custom Elements.
A Context API for Custom Elements would need to tell users to ensure that they define their elements up front to avoid CE upgrade throwing in a wrench (I've seen how Lit handles that case, and unfortunately it adds a lot of code, but it is doable).
All non-CE framework have provide a guarantee on component instantiation order, so non-CE frameworks never have to content with upgrade order, which makes Context APIs easier to implement reliably.
If we do support function components, ...
... and Signals
are built in (and assuming that Signal
s expose a tree of Effect
s, which they should!) then a framework could easily traverse the effect tree (instead of the DOM) and totally avoid Custom Element upgrade order issues because the Effect
tree will always be reliable in this case.
But this would require that html
returns a function so that it can be called in context:
html
return valueThe new return value would return a function that could be used like so:
function SomeSolidComponent() {
const template = html`<div>
<${ComponentThatConsumesTheProvider} />
</div>`
typeof template === 'function' // true
return html`
<${ContextProvider} value=${123}>
${template}
</${ContextProvider}>
`
}
And if you need the div
ref, then it would be slightly less convenient, but would allow people the opportunity to connect templates together while keeping them all in the same reactive context without having to manually wire up reactivity, and without having to manually dance around creating templates with template parts:
function SomeSolidComponent() {
let node
const template = html`<div>
<${ComponentThatConsumesTheProvider} />
</div>`
queueMicrotask(() => {
console.log(node instanceof HTMLDivElement) // true
})
return html`
<${ContextProvider} value=${123}>
${() => ({node} = template())}
</${ContextProvider}>
`
}
And with the simplified syntax from above:
return html`
<{ContextProvider} value={123}>
{({node} = template())}
</{ContextProvider}>
`
Small random thought, but will we ever switch to using built-in modules instead of globals, f.e.
import {html} from 'std:html'
or similar?
There might be some considerations for what non-CE frameworks need (function components? reactive signals-based contexts?), and what end html
users need (easy access to the DOM without having to write up additional render logic?).
We want to make things easy so that typical devs who are focused on just building an app don't feel like the tool the browser gave them is too difficult.
People complained that vanilla Custom Elements were not convenient to use compared to non-CE framework components. This is a good chance to prevent people from thinking that html
is too difficult to use because it requires writing a library (or dancing with the render
function).
At the end of the day, it'll be useful even if we have to dance around with <template>
to make it work, but how much easier can we make it?
What's does the simplest usage look like so people can just write the simplest code out of the box?
Do we need to actually expose {strings, values, kind}
?
If the browser does html
updating well internally, exposing DOM for easy access, and Parts for custom wiring of data to that DOM by any other means, is that not enough?
Do we need Parts at all? Can the browser simply return only the DOM, and keep it updated whenever we re-run the template or update signal values? Why do we need anything more?
I'm just thinking as an app author, that's all I want html
to do, just give me the DOM, keep it updated, I'm on my way.
Related to many template, template instantiation, and DOM parts discussions (but especially https://github.com/WICG/webcomponents/issues/777, https://github.com/WICG/webcomponents/issues/682, and https://github.com/WICG/webcomponents/issues/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:
Idea
Add an
HTMLElement.html
template tag and anHTMLElement.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:Templates would be rendered to an element with the
HTMLElement.render()
method: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.
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:
Prior art
The general shape of this API has been popularized by several userland libraries including hyperHTML, lit-html, and FAST.