- From: Joe Pea <notifications@github.com>
- Date: Fri, 11 Oct 2024 21:43:27 -0700
- To: WICG/webcomponents <webcomponents@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <WICG/webcomponents/issues/1069/2408382441@github.com>
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, so 👍. @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 > ```ts > const {html} = HTMLElement > html`<ul> > ${items.map((item) => html`<li>${item.name}</li>`)} > </ul>` > ``` How does this work with signals though? > ```ts > const {html} = HTMLElement html`<ul> ${items.get().map((item) => html`<li>${item.name}</li>`)} </ul>` ``` It does seem that this is only solvable with new `{expr}` syntax as shortcut for `${() => expr}`? What if Signals came out first: would it be plausible to make a rule that a function value will always be unwrapped, so if you wanted to actually set a function value, you'd need to wrap it: `.someFunction=${() => theFunction}`. Otherwise `.someFunction=${theFunction}` would get unwrapped in an effect every time. The rule would be that function values always get unwrapped in an effect, which could be a rule that we would be able to make if `Signals` were already native to the language. But not sure that's ideal? # TypeScript Plus 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, unless we come up with the above rule if we assume that `Signals` are released first before an `html` template tag is released. 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, and in order to support that we'd have to agree that all values need to be passed as function wrapper for signals so that we can use `ReturnType` to reliably determine if `() => number` works when `.someNumber` expects a number value. # `html` return value Lastly, what I would really really want, is this, which is more convenient and usable: ```js 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 do agree on `DOM Parts` API, then something like this might be still convenient: ```js const signal = new Signal(0) const [div, parts] = html`<div>value: {signal.get()}</div>` console.log(div instanceof HTMLDivElement) // true ``` or: ```js const signal = new Signal(0) const part = html`<div>value: {signal.get()}</div>` const {node: div} = part console.log(div instanceof HTMLDivElement) // true ``` Maybe the last options is best as Parts API already has `.node`. # 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: ```js 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: ```js 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 `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*: # back to `html` return value The new return value would be like this: ```js function SomeSolidComponent() { const template = html`<div> <${ComponentThatConsumesTheProvider} /> </div>` return html` <${ContextProvider} value=${123}> ${template} </${ContextProvider}> ` } ``` And if you need the `div` ref, then its slightly less cumbersome, but allows people the opportunity to connect templates together while keeping them all in the *same reactive context* without having to manually wire up reactivity contexts: ```js 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: ```js 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. ```js 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?). -- Reply to this email directly or view it on GitHub: https://github.com/WICG/webcomponents/issues/1069#issuecomment-2408382441 You are receiving this because you are subscribed to this thread. Message ID: <WICG/webcomponents/issues/1069/2408382441@github.com>
Received on Saturday, 12 October 2024 04:43:31 UTC