Skip to content
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

Open
justinfagnani opened this issue Aug 16, 2024 · 25 comments
Open

[templates] A declarative JavaScript templating API #1069

justinfagnani opened this issue Aug 16, 2024 · 25 comments

Comments

@justinfagnani
Copy link
Contributor

justinfagnani commented Aug 16, 2024

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.

@justinfagnani justinfagnani changed the title [templates] A JavaScript templating API [templates] A declarative JavaScript templating API Aug 16, 2024
@EisenbergEffect
Copy link

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
Copy link
Contributor Author

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.

@justinfagnani
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link

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
Copy link
Contributor Author

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
Copy link
Contributor

rictic commented Aug 17, 2024

I know that Google's internal security team has replaced a bunch of innerHTML calls in our internal codebase with Lit to improve security. Putting this in the platform seems like a solid thing to point people to when they'd otherwise reach for innerHTML, would greatly improve security and (for updates) performance too.

@NullVoxPopuli
Copy link

NullVoxPopuli commented Aug 18, 2024

I have a concern, but I'm not sure where it belongs -- but with reactive rendering w/ Signals -- and maybe this is something that a library/framework would use to solve my concern, but -- given this example:

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
Copy link

au5ton commented Aug 18, 2024

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
Copy link

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

@o-t-w
Copy link
Contributor

o-t-w commented Aug 18, 2024

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

@justinfagnani
Copy link
Contributor Author

justinfagnani commented Aug 18, 2024

@o-t-w JSX would require major changes to JavaScript via TC39. A tagged template literal based system wouldn't.

But we've seen compilers that transform JSX to tagged template literals before, so frameworks that use JSX could target this API, or if JavaScript ever does add a JSX-like syntax, it could be made to work with this template system.

@justinfagnani
Copy link
Contributor Author

@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
Copy link

EisenbergEffect commented Aug 18, 2024

Getting back to the issue that @NullVoxPopuli raises...what if we could introduce the idea of lazy evaluation to tagged template literals? For example, ${value} is the same eager evaluation that we have today, but {value} could be lazily evaluated. Essentially, it would get converted to something like ${() => value} under the hood. It's maybe too late to change tagged template literals, as that would be a break, but maybe a new literal type with three backticks or similar could enable this. Or what if the tagged template function itself could opt into this interpretation somehow?

Obviously, this then becomes a TC39 thing, which complicates matters. But it seems to me that tagged template literals could be a lot more powerful if that part of the language were explored more.

@justinfagnani
Copy link
Contributor Author

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

@sorvell
Copy link

sorvell commented Aug 21, 2024

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

@sorvell
Copy link

sorvell commented Aug 21, 2024

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

@mdesantis
Copy link

Hello everyone, I have some (most likely) naive questions:

  1. How would this integrate with events? 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>
`);
  1. How would conditionals look?
  2. How would loops look?

@NullVoxPopuli
Copy link

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

@justinfagnani
Copy link
Contributor Author

@mdesantis

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

  1. @ is a pretty common prefix for events (used in Vue and others), while . looks like property access.
  2. ., @, 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.
  3. ., @, 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.

  1. How would conditionals look?
  2. 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:

  1. Template as expressions returning values
  2. Composition of templates
  3. Support for iterables
  4. Relying on JavaScript for expressions and control flow

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.

@justinfagnani
Copy link
Contributor Author

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

@littledan
Copy link

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.

@justinfagnani
Copy link
Contributor Author

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

@justinfagnani
Copy link
Contributor Author

justinfagnani commented Sep 26, 2024

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

@trusktr
Copy link
Contributor

trusktr commented Oct 12, 2024

I'm breaking my coment into a few sections to make it easy to reply to whichever part you're interested in:

Lit Syntax

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.

Signals

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

TypeScript

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 value

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

Context APIs

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:

function components

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

Back to Contexts

... and Signals are built in (and assuming that Signals expose a tree of Effects, 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:

back to html return value

The 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}>
  `

ES Modules

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?

TLDR:

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests