WICG / webcomponents

Web Components specifications
Other
4.34k stars 373 forks source link

[dom-parts] Declarative syntax for creating DOM Parts #1003

Open tbondwilkinson opened 1 year ago

tbondwilkinson commented 1 year ago

Branched from #999

@bahrus <template> is more oriented towards populating a piece of HTML that can be later cloned by JavaScript. It does not seem fit to indicate nodes of interest.

There are few concerns with using any sort of Node (<template> or otherwise) to mark ranges. Using a new Node, and nesting child nodes in that node, modifies the DOM structure in more meaningful ways than adding PI/comments. If the node(s) of interest are now children of some other node, they are no longer children of their parent in the desired rendered output. Imagine a list where only one child is dynamic - if that child and no others are nested in another Node, your CSS selectors become quite a bit more difficult to implement. Today, applications do not nest their dynamic content in special nodes, so introducing a new node and nesting any dynamic content is probably a non-starter for most applications, which makes it a non-starter.

My general concern with <template> in particular is that although in this case we're using the same word, "template" in <template> means a template of HTML that can be cloned and updated. "template" in DOM parts refers to a user-authored template that has dynamic content. I would hesitate to represent a dynamic content point with <template>, as it just doesn't feel like a good fit for that language feature.

Other comments below.

A. Hello, <?child-node name?>Adam<?/child-node?>

With the minor nit that processing instructions are not HTML elements and so therefore do not have end tags like a normal element would, and also do not have attributes like a normal element would, yes this is the current proposal. I've slightly corrected your example.

B. Hello <template prop=name start/>Adam<template end/>

FYI the syntax for ending <template> nodes is </template> not <template end/>.

<template> elements do not render in the DOM initially, they require JavaScript to clone and insert into the DOM, see the example here. So by using a <template>, you wouldn't render "Adam". If you wanted to render "Adam", or some other dynamic value in that position, in your case you would need to locate the <template> and then prepend or append the content before or after the template. That seems like an odd API, and prevent server-side rendered content from being rendered until JavaScript has had the chance to do that hydration, which isn't ideal. If the browser would render "Adam", that of course breaks the <template> spec. If we make <template> have a new mode where "Adam" is rendered... why would we use <template> at all in that case.

Caveat that declarative shadow DOM does declaratively insert <template> and its content, but that's probably a tangent.

C. Hello <template prop=name node-count=1 level=0/>Adam

<template> nodes need to be closed, so I'm not sure how you're imagining this would work.

D. Hello <?child-node prop=name node-count=1 level=0 ?>Adam

This is in theory possible but requires a lot of book keeping to continually update the node-count attribute after any content change. You can imagine examples where a range is replaced by a series of nodes, rather than just one. I'm not sure what "level" here indicates.

tbondwilkinson commented 1 year ago

Additionally, you mention that the benefit of <template> is that it's easier to query for. It's a non-goal for these markers to be easily queried. The performance benefit of adding these markers into the DOM is that the browser can better parse and cache them, making their lookup constant time without needing to do any query or DOM walk. So the fact that PI/comments are not particularly easily to query is probably more of a feature than a bug.

If someone wanted lightweight nodes wrapping content that they can easily query, that's precisely a <span> with ids or classes. The idea is that we want to speed up hydration of DOM after a render.

You also mention that by only having an opening marker without a closing marker, we would save bytes. It can be hard to speculate on this type of thing because compression algorithms like gzip and brotli make bytes in not equal bytes out, and so repeated content does not always lead to linearly many output bytes. I'd encourage you if you're curious to run a sample piece of HTML with and without end markers into gzip and brotli and see what the size difference actually is.

sashafirsov commented 1 year ago

as @keithamus stated

The main objection (attempting to summarise from the notes) is that {{}} may leave artefacts in the rendered DOM, as there's no default value. What does

{{name}}
do if name isn't supplied in the data? For templates this is not an issue as they're not user visible until processed.

That is why there is a need for

keithamus commented 1 year ago

WCCG had their spring F2F in which this was discussed. Present members of WCCG identified an action item to take the topic of DOM Parts and break it out into extended discussions. You can read the full notes of the discussion (https://github.com/WICG/webcomponents/issues/978#issuecomment-1516897276) in which this was discussed, heading entitled "DOM Parts API".

As this issue pertains to DOM parts, I'd like to call out that https://github.com/WICG/webcomponents/issues/999 has been raised for extended discussions and this topic may be discussed during those sessions.

EisenbergEffect commented 1 year ago

I'd like to propose an alternative syntax and semantics for DOM Parts that I think would:

The idea is that we would use {{}} for values and {{#tag}}{{/tag}} for blocks. This is borrowed directly from the popular handlebars/mustache libraries.

What I propose, is to pair this with a parsermode attribute on template and body which tells the HTML runtime how to interpret the values between the open/close tags.

In prerender mode, the browser would directly render the contents of the tag, as if the tag wasn't there, but it would also collect the part.

Example 1: DSD with Parts that have literally rendered values

<template shadowrootmode="open" parsermode="prerender">
  This is an {{example}} of how we could use the {{same}} syntax for DSD DOM Parts as {{live}} templates.
  <a href="./some/{{link.html}}">We can even use it in attributes.</a>
  {{#tag}}
    <div>
      Nested parts {{work fine}} too!
    </div>
  {{/tag}}
</template>

Example 2: Server rendered parts in the body.

<body parsermode="prerender">
  This should also work in the {{body}}.
</body>

In the above examples, the content between {{}} and {{#tag}}{{/tag}} would be literally rendered by the browser. The curlys themselves would be removed. As part of this process, the parts would be captured and stored with the template/body for use later by libraries.

Example 3: Templates with custom bindings

<template parsermode="custom">
  This is an {{this.example}} of how we could use the {{this.same}} syntax for DSD DOM Parts as {{this.live}} templates.
  <a href="./some/{{this.link}}">We can even use it in attributes.</a>
  {{#tag}}template#myTemplate{{/tag}}
</template>

<template id="myTemplate" parsermode="custom">
  <div>
      Nested parts {{this.workFine}} too!
  </div>
</template>

Example 4: Body with custom bindings

<body parsermode="custom">
  This should also work in the {{this.body}}.
</body>

In the above examples, the browser would not render the content between {{}} and {{#tag}}{{/tag}}. It would simply extract the parts, and store the content of the tags on the part itself, for further processing by the library/framework. The content is open for interpretation by the chosen library (custom).

In the future, once the reactivity model, expression syntax, block rendering (conditional, list, etc.) are all worked out, we can add a new mode value that adds the additional behavior of setting up bindings against the parts and automatically handling updates. (e.g. mode="live").

I can't claim to have worked out all the edge cases here, but I wanted to drop this in the issue to help move the conversation along more. In our Spring F2F @rniwa thought that there might be a way to support {{}} in the body and there was also discussion indicating that it would be nice to have a single syntax for parts in both the DSD and live contexts. So, this is my quick take on that.

bahrus commented 1 year ago

FYI the syntax for ending \<template> nodes is \</template> not \<template end/>.

Thanks, @tbondwilkinson , for your helpful explanation, and apologies for my lack of clarity. I think my lack of clarity is my bad, as I think both you and @justinfagnani may have read that the same way, which was not my intention.

Most especially, thanks for presenting your promising sounding ideas for an important advance forward for the web.

Given my track record, I doubt my clarifications below will help, but giving it a college try anyway. Thanks for your patience!

I intended:

<template prop=name start/>

to be shorthand for the proper HTML 5 syntax:

<template prop=name start></template>

The purpose of the start and end attributes in my attempt for a 1-1 correspondence, was to provide a similar way as the processing instructions would provide as far as indicating where the range started and ended. I didn't intend to imply that the browser currently recognizes those attributes in any way but thanks for the thoughtful tip :-).

I was literally proposing a one-for-one swap of processing instruction comments with empty template elements (open/closed tags, with nothing inside, which I was hinting maybe the platform could allow self-closing template tags like it does for div's), we replace each and every processing instruction with precisely one self closing / empty template tags such as above. You asked for alternatives, and that's the only alternative I could think of which wouldn't affect what the browser renders (and which works in the table element). I like @EisenbergEffect 's suggestion that we be bolder and look at deeper changes to the browser if it makes sense. I was perhaps being too timid in assuming that the alternatives had to involve minimal changes to how the browser currently renders.

Correction/Update: Even the div tag does weird things when trying to make it self closing (to my surprise), so I definitely see how my attempt to make the markup look more palatable caused significant confusion about what I was proposing. Apologies for that.

Now if the processing instructions are meant, as part of this proposal, to add any additional information, other than simply saying "behold, a tag that you should turn into a part", like which field name from the host the data is coming from, we could use the full power of HTML and provide that metadata as attributes (if we use self-closing or at least empty template tags with nothing inside in a one-for-one swap with each processing instruction), queryable by developers using css/xslt when hydrating the data out from server rendered content, and possibly other mischief like reconstructing the original template, and search engines could also use that (standardized) notation to help index the content of web pages.

Your comment above suggests that it is applicable to your proposal to include metadata in the processing instruction, but as I read more about the proposal, I'm not sure it would be, so maybe I'm reading too much into your proposal? Meaning maybe the advantages I see of using a template element are moot, if we don't include such metadata in the instructions. I guess I'm a little confused about that point.

Perhaps none of the uses cases I suggest above have been adopted (or even considered?) by the big frameworks, so I understand that as a result, my suggestion is a non-starter. Again, those use cases are:

  1. Hydrating/extracting the data from the server-rendered HTML, so that we don't require a separate download of the data, which would need to be parsed and aligned with the corresponding HTML nodes.
  2. Reverse engineering the HTML to infer the template used to generate the HTML.
  3. Providing search engines the ability to correlate information displayed to the user with data attributes like they can do with microdata (itemprop attributes).

Both you (I think) and @justinfagnani have indicated confidence that the cost of a single text node is significantly less than the cost of a single empty / self-closing inert template HTML Element with nothing inside, thus my alternative proposal would impose a heavier load on the browser. That was my suspicion, but a quick performance test I did indicated similar performance metrics, but I'm not enough of an expert (and lack the patience) to do a thorough analysis of the costs. However, I think you and @justinfagnani understand the inner workings of the browser enough to confidently predict that there is a significant performance gap, so I will leave that there. I should note that at various points in the history of the web, there have been introduced tags/attributes that do help provide metadata, which surely comes at some cost to performance, so the question lingers in my mind, even if there is a cost to performance, if those benefits could outweigh the costs, but I'm clearly in a minority of one on this issue, so again, I see why the idea is being dismissed as a non-starter.

As for the usefulness of the count, I remember now that my final intentions of using it were to help determine whether some content between an empty template element used to convey an opening section (similar to the opening processing instruction) and the closing open/closed/self-closing template tag indicating where that section ends, if any of the content inside may have "given birth" to addition nodes, meaning a re-running of the binding needs to take place due to a suspicion of staleness.

I think if performance is of paramount, and if my suspicions are correct, that even processing instructions impose a small, linear cost, so that the more there are, the worse the performance, that it should be possible to "share" inner processing instructions, so that they serve both as the end of the previous section, and the beginning of the next. Only the first and last nodes of a for-each loop would require beginning (and ending) comment nodes. But again, probably not something the big frameworks have tried, so probably a non-starter to consider (I have no idea what they've tried and found wanting).

Getting into the intricacies of how the level could be used is probably not worth the effort to explain, as I think I was addressing use cases that appear to be of little interest to anyone else.

bahrus commented 1 year ago

Not to detract from the exciting proposal @EisenbergEffect has laid out that seems to address all concerns, but also seems to require significant changes to rendering by the browser, I suppose addressing @justinfagnani criticisms of my suggestion of the template element as not being semantically meaningful, I would suggest, as an alternative to processing instructions, we could use the already existing self-closing meta tag, which seems to already be used for search engines quite extensively, and already supports the itemprop attribute (and like processing instructions doesn't render anything).

Unfortunately, unlike the template element, the table element spits it out:

    <table>
        <tr>
            <meta name="hello">
            <td>hello</td>
        </tr>
    </table>

gets rendered as:

<meta name="hello">
    <table>
        <tr>
            <td>hello</td>
        </tr>
    </table>

But if we are talking bold actions, maybe this could be changed, to behave more like the template element?

bahrus commented 1 year ago

Having taken some time to reflect on this issue, I realize I was starting to do something I find quite annoying when others do: Critique some request for something other people desperately want, which I have no need or desire to use (I thought I did, which is why I was starting to get involved, but on further reflection, realize I don't).

I'm 100% Team Template Instantiation With Built In Support For Moustache Syntax that supports nested loops, conditionals, etc. The sooner the better. I trust the browser vendors can find a way to take server rendered content that is consistent with the output that would be generated by template instantiation, and find the best way, with or without the help of markers, to take that generated content, and apply the template with future updates, in as efficient a way as possible. Perhaps the browser vendors can consult with how frameworks do it for tips, also perfectly fine as far as I'm concerned, especially if the result is better performance / ergonomics, etc.

If frameworks don't want to compile their DX optimized syntax to that template syntax (under the hood), that's their prerogative, and if they want to request some other support, also fine, as long as it doesn't block what I want.

For my needs / desires:

  1. Hydrating/extracting the data from the server-rendered HTML, so that we don't require a separate download of the data, which would need to be parsed and aligned with the corresponding HTML nodes.
  2. Reverse engineering the HTML to infer the template used to generate the HTML.
  3. Providing search engines the ability to correlate information displayed to the user with data attributes like they can do with microdata (itemprop attributes).

The only things I think I would ask for is the following, which I'm mulling over before making it a full proposal:


A good percentage of websites use microdata .

I think nudging developers to make use this feature by making it super-easy, when working with template instantiation, would have beneficial impact for the web and society in general.

My (hidden) agenda [TODO]

The specific syntax of this proposal is not meant to be read as a particular endorsement of any particular syntax (i.e. handlebar vs moustache), and is up in the air, as far as I know, so please interpret the examples "non-literally".

Because there's a performance cost to adding microdata to the output, it should be something that can be opt-in (or opt-out).

But basically, for starters, there would be an option we could specify when invoking the Template Instantiation API: emitMicrodata.

What this would do:

Let's say our (custom element) host object looks like:

const host = {
    name: 'Bob'
    eventDate: new Date()
}

And we apply it to the template:

<template>
    <span>{{name}}</span>
</template>

then with the emitMicrodata setting it would generate:

<span itemprop=name>Bob</span>

Formatting

If template instantiation supports formatting:

<template>
    <time>{{eventDate.toLocaleDate|ar-EG, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }}}</time>
<template>

it would generate:

    <time itemprop=eventDate datetime=2011-11-18T14:54:39.929Z>11/18/2011</time>

Now let's talk about the dreaded interpolation scenario.

<template>
    <div>Hello {{name}}, the event will begin at {{eventDate}}</div>
</template>

Template instantiation would generate:

<div>Hello <meta itemprop=name/>Bob<meta content>, the event will begin at <meta itemprop=eventDate itemtype=date content=2011-11-18T14:54:39.929Z>11/18/2011</div>
bahrus commented 1 year ago

This is my (evolving) proposal.

bahrus commented 1 year ago

My proposal is now quite different. Please give another look, I think this getting closer to what we want (in my opinion, obviously).

bahrus commented 1 year ago

There's an unfortunate stipulation I had missed in my proposal:

According to MDN:

The itemtype attribute can only be specified on elements which have an itemscope attribute specified.

Personally, I think this should be lifted (thinking about primitive types), but that's an uphill battle.

So I've updated the proposal so that template instantiation would only dynamically set itemprop, itemref and meta tags with content, and not emit any itemtypes whatsoever, leaving that part up to the developer.

The proposal is extremely light now, and I recognize it could be done via tooling, that would compile to the template syntax, whatever that syntax is, if there are any objections to it.

But I do think it is possible, based on what I know, to fully define a hierarchical tree beyond what the HTML structure provides, with the help of meta tags, and itemref/itemscope and id's, without needing to introduce yet more new syntax in the HTML (processing instructions), and without violating proper HTML decorum. And sticking with moustache syntax. Please let me know if I'm missing anything.

bahrus commented 1 year ago

One serious shortcoming where microdata falls short as a replacement for processing instructions is in specifying attributes that we want to turn into a part.

Maybe, if the performance is significantly improved by using processing instructions for this, it could be an interim solution. I kind of find it difficult to picture servers emitting these processing instructions long term just for that, and seems like one of those things that would get introduced and rarely used and quickly forgotten.

But the limitation is really a significant shortcoming, in my view, of the microdata standard. Much valuable information that would be useful for search engines (and hydrating) is contained in aria- attributes as well as such as attributes like title, value, and alt.

This one would require a new global microdata attribute:

<img itempropmap="photoCaption: alt; photoDescription: title; photographer: aria-label"
 alt="Bear rummaging through garbage" 
aria-label="Photo by Simone de Beauvoir" 
title="Click on photo for more info"  >
clshortfuse commented 12 months ago

I've been using {prop} and {deep.prop} originally, but found better value in restructuring the concept around query, subqueries and processors. I'm now considering adding scope In other words:

<x-card>
  <h1 x-if={!!firstName}>{firstName}</h1>
  <x-progress x-if={!ready} value={expressions:computedStatus}></x-progress>
</x-card>

Restructured {!!firstName} means !!firstName is a query, firstName is the subquery. and !! is a processor. In terms of rendering, firstName is a repeated subquery, therefore only looked up once, not twice. Assume computedStatus has a complex getter that does math operations (expression).

I know lit and vue have their own processors, like ?. The ability to register our own processors would be interesting, since covering all use-cases would be impossible. Instead of symbols (/[^a-z]+/, perhaps even function-like parentheses like toBoolean(firstName) as processors.

I've noticed I've been "polluting" my prototype with expressions (computedStatus). So I've been considering adding something of a scope to each query. Not specified, it would just use this. But a custom scope could be useful. There's no reason to expose computedStatus. It could possible be better scripted as {expressions:computedStatus} (with a shorthand probably as e:computedStatus).

The resulting syntax would be { processors scope subquery }