-
Notifications
You must be signed in to change notification settings - Fork 375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[templates] A declarative JavaScript templating API #1069
Comments
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. |
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. |
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. |
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 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 |
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);
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 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. anywho, my 2c |
@NullVoxPopuli From what I understand, this would be an incorrect usage of Signals. I believe you're supposed to use See: https://github.com/tc39/proposal-signals?tab=readme-ov-file#example---a-signals-counter |
Usage of |
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 #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:
An API to pass in the external scheduler could take the form of extra options to 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, 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 |
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? |
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 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 |
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>
`);
|
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)
Having a way for template-literals to implicitly imply |
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 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:
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.
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 I think an explicit syntax is much, much better.
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 |
It's not just about character count to me, but also about author intent. 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 The transform (a bit simplified) breaks down like this: First, we have the 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 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 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 ( 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. |
I'm breaking my coment into a few sections to make it easy to reply to whichever part you're interested in: Lit Syntax
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 Plus, because Lit's The only real problem I would change in Lit In contrast, React 19's new Custom Elements support will have lots of unexpected behavior. One one custom elements you may write SignalsGiven this
How does this work with signals?
Indeed it seems only with But a question is, what if 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: 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 But I'm not sure that's ideal... TypeScriptPlus it, unless F.e. 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 People might complain about the ergonomics. So maybe
|
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:
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.
The text was updated successfully, but these errors were encountered: