Skip to content

Add <template for> for declarative out-of-order streaming#11818

Open
foolip wants to merge 31 commits into
mainfrom
foolip/contentmethod
Open

Add <template for> for declarative out-of-order streaming#11818
foolip wants to merge 31 commits into
mainfrom
foolip/contentmethod

Conversation

@foolip
Copy link
Copy Markdown
Member

@foolip foolip commented Oct 22, 2025

Fixes #11542.

  • At least two implementers are interested (and none opposed):
  • Tests are written and can be reviewed and commented upon at:
  • Implementation bugs are filed:
    • Chromium: …
    • Gecko: …
    • WebKit: …
    • Deno (only for timers, structured clone, base64 utils, channel messaging, module resolution, web workers, and web storage): …
    • Node.js (only for timers, structured clone, base64 utils, channel messaging, and module resolution): …
  • Corresponding HTML AAM & ARIA in HTML issues & PRs:
  • MDN issue is filed: …
  • The top of this comment includes a clear commit message to use.

(See WHATWG Working Mode: Changes for more details.)


/indices.html ( diff )
/infrastructure.html ( diff )
/parsing.html ( diff )
/scripting.html ( diff )
/timers-and-user-prompts.html ( diff )

@foolip foolip changed the title Add <template contentmethod declarative out-of-order streaming Add <template contentmethod> for declarative out-of-order streaming Oct 22, 2025
@foolip foolip marked this pull request as draft October 22, 2025 15:05
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
@foolip
Copy link
Copy Markdown
Member Author

foolip commented Nov 6, 2025

I've made some additional changes but things don't quite make sense yet. The direction I'm heading in is:

  • When the parser encounters an <template contentmethod> element, it doesn't insert it.
  • In insert an element at the adjusted insertion location, if we're about to insert an element with a contentname attribute into such a <template> element:
    • Find the target element among the descendents of the element that the <template> element was in. That target is kept as a bookkeeping slot, the tree traversal only happens once.
    • For contentmethod=replace, remove the target and actually insert. (For the following cases, the element isn't inserted.)
    • For contentmethod=replace-children, remove the children.
    • For contentmethod=prepend, save the current first element of the target.
    • For contentmethod=append, there's nothing to do here.
  • In appropriate place for inserting a node further adjust the location based on contentmethod after the existing foster parenting adjustments. (This happens before the above step, but isn't relevant in that case.)
  • In insert an element at the adjusted insertion location, if we're about to insert into an element with a contentname attribute and the bookkeeping all checks out:
    • For contentmethod=replace-children and contentmethod=append, just append.
    • For contentmethod=prepend, use the saved first element of the target.

There are options for where to store the bookkeeping. I initially put it on the <template> element but now think it might be easier to follow if the bookkeeping goes on the element with the contentname attribute. An implementation might keep it as extra information in the stack of open elements.

@noamr
Copy link
Copy Markdown
Collaborator

noamr commented Nov 6, 2025

I've made some additional changes but things don't quite make sense yet. The direction I'm heading in is:

  • When the parser encounters an <template contentmethod> element, it doesn't insert it.

contentmethod also needs to be valid

  • In insert an element at the adjusted insertion location, if we're about to insert an element with a contentname attribute into such a <template> element:

    • Find the target element among the descendents of the element that the <template> element was in. That target is kept as a bookkeeping slot, the tree traversal only happens once.
    • For contentmethod=replace, remove the target and actually insert. (For the following cases, the element isn't inserted.)
    • For contentmethod=replace-children, remove the children.
    • For contentmethod=prepend, save the current first element of the target.
    • For contentmethod=append, there's nothing to do here.
  • In appropriate place for inserting a node further adjust the location based on contentmethod after the existing foster parenting adjustments. (This happens before the above step, but isn't relevant in that case.)

  • In insert an element at the adjusted insertion location, if we're about to insert into an element with a contentname attribute and the bookkeeping all checks out:

    • For contentmethod=replace-children and contentmethod=append, just append.
    • For contentmethod=prepend, use the saved first element of the target.

It also needs to fail if that first element is no longer a child of the target.

There are options for where to store the bookkeeping. I initially put it on the <template> element but now think it might be easier to follow if the bookkeeping goes on the element with the contentname attribute. An implementation might keep it as extra information in the stack of open elements.

Yea makes sense, that way you don't have to deal with grandparents.

@foolip foolip marked this pull request as ready for review November 8, 2025 12:13
@foolip
Copy link
Copy Markdown
Member Author

foolip commented Nov 8, 2025

I've now rewritten a lot of this to make it match my previous comment, and I think it's in good enough shape for review now.

I left two inline issues:

If target's first child was moved or removed, the element will be appended to target below. Should the node be dropped instead, or should we update content target first child and keep inserting before it?

and

Patching head from within head is not possible but could easily be supported.

@noamr for the first one, you said we should fail, but what would that mean?

@noamr
Copy link
Copy Markdown
Collaborator

noamr commented Nov 8, 2025

I've now rewritten a lot of this to make it match my previous comment, and I think it's in good enough shape for review now.

I left two inline issues:

If target's first child was moved or removed, the element will be appended to target below. Should the node be dropped instead, or should we update content target first child and keep inserting before it?

and

Patching head from within head is not possible but could easily be supported.

@noamr for the first one, you said we should fail, but what would that mean?

It means no further content is prepended.

@foolip
Copy link
Copy Markdown
Member Author

foolip commented Nov 9, 2025

Okay, should that state be sticky, or what happens if the nodes later realign so that the check passes?

@noamr
Copy link
Copy Markdown
Collaborator

noamr commented Nov 10, 2025

Okay, should that state be sticky, or what happens if the nodes later realign so that the check passes?

Yea I think it errors the whole thing

@noamr
Copy link
Copy Markdown
Collaborator

noamr commented Nov 11, 2025

Missing pieces following the TPAC session:

  • Applying patches inside the fragment parser (e.g. innerHTML) should be guarded in the same way as declarative shadow root, to avoid breaking assumptions made by userland sanitizers like DOMPurify.
  • Find a way to notify in case of an error instead of failing silently. e.g. dispatch some error event on the document with a reference to the detached template element.
  • Ensure that this works correctly when patching the child text nodes of scripts/styles, given that those elements have been "closed" before.

@foolip
Copy link
Copy Markdown
Member Author

foolip commented Nov 14, 2025

I've given some thoughts to error handling for contentmethod=prepend. Here's an example of the problem:

<!doctype html>
<body>
<div contentname=foo><span id=refnode>will be removed</span></div>

<template contentmethod=prepend>
  <div contentname=foo>
    <p>this element is inserted</p>
    <!-- the script removes the "content target first child" node -->
    <script>window.refnode = document.getElementById('refnode'); refnode.remove();</script>
    <p>reference node is gone, can this element be inserted?</p>
    <!-- put the reference node back -->
    <script>document.querySelector('[contentname=foo]').appendChild(refnode);</script>
    <p>is this OK because the reference node is back?</p>
  </div>
  <div contentname=foo>
    <p>is this fine because we saved a new reference node?</p>
  </div>
</template>

Options on what constitutes an error:

  • Reference node is not a child of target
  • Reference node has no parent (such that any parent would do)

Options on handling:

  • Error is transient and if the error condition goes away, we keep inserting
  • Error state is sticky to the <div contentname=foo>, skipping the rest of that patch
  • Error state is sticky to the <template contentmethod=prepend>, skipping the rest of the current patch and any others in the template
  • For sticky error states, it could be set either when the condition is broken (refnode.remove()) or when we discover that it is broken (when inserting the node after the script)

Options on reporting:

  • Nothing, nodes are silently dropped, at most with devtools warnings
  • Queue a task to fire a bubbling "contenterror" event at the template element's would-be parent. Could also be the would-be parent's root, more like the "unhandledrejection".
    • Either one event for every failed insertion or
    • one event for the first error or summarizing all errors.

It looks like the parser currently never fires events while it's still running, the only events are "DOMContentLoaded" and "load". Anything else that seems be fired by parsing is actually triggered by some other algorithm run as a side effect of what the parser does.

I don't think that parse errors are a good fit either, because the error can't easily be expressed in terms of the input, but the shape of the DOM tree.

My thinking now is that we should instead guarantee that the nodes are inserted, that we don't discard node midstream. For contentmethod=prepend the model I think could work is maintaining an insertion point within the target element that is updated if children are removed.

@foolip
Copy link
Copy Markdown
Member Author

foolip commented Nov 20, 2025

Applying patches inside the fragment parser (e.g. innerHTML) should be guarded in the same way as declarative shadow root, to avoid breaking assumptions made by userland sanitizers like DOMPurify.

This is controlled by https://dom.spec.whatwg.org/#concept-document-allow-declarative-shadow-roots. We should probably rename this to cover both of these behaviors.

Comment thread source Outdated
@foolip foolip force-pushed the foolip/contentmethod branch from af700a0 to 1e56dfb Compare December 4, 2025 12:37
@foolip foolip changed the title Add <template contentmethod> for declarative out-of-order streaming Add <template for> for declarative out-of-order streaming Mar 9, 2026
@foolip
Copy link
Copy Markdown
Member Author

foolip commented Mar 9, 2026

I've pushed a change to rewrite this as <template for> using processing instructions <?marker>, <?start>, and <?end>.

This depends on three other PRs:

Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
<p>If <var>scope</var> is a <code>template</code> element, then set <var>scope</var> to
<var>scope</var>'s <span>template contents</span>.</p>

<p class="note">This is to support patching inside <code>template</code> elements.</p>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<p class="note">This is to support patching inside <code>template</code> elements.</p>
<p class="note">This is to support patching inside <code>template</code> elements. It
would work for normal templates or declarative shadow roots, however it effectively disallows
nested patches, as the <span>template contents</span> is separate from the <span>insertion
target</span></p>

Comment thread source Outdated
Comment thread source
<li><p>Let <var>nextNode</var> be <var>currentNode</var>'s
<span>next sibling</span>.</p></li>

<li><p><span data-x="concept-node-remove">Remove</span> <var>currentNode</var>.</p></li>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can have impllications if the node is an iframe with a pagehide, e.g. it can synchronously add more siblings or change things about the document. It's perhaps OK but an alternative would be to accumulate all the nodes before the <?end> and then remove them in one go.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you remove them all at once? That's not a primitive that we have. How do we account for changes here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes in there won't have any effect. You'd have accumulated a list of nodes in this step, and then when we see <?end> we remove all of those one by one. If there is a pagehide or whatnot for one of them it wouldn't be able to change that list.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is probably a good idea.

We probably still need to add guards though because concept-node-remove for instance asserts node's parent is non-null.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, those should generally behave like Node.prototype.remove()

Comment thread source
[<span>HTMLConstructor</span>] constructor();

readonly attribute <span>DocumentFragment</span> <span data-x="dom-template-content">content</span>;
[<span>CEReactions</span>, <span data-x="xattr-Reflect">Reflect</span>="<span data-x="attr-template-for">for</span>"] attribute DOMString <dfn attribute for="HTMLTemplateElement" data-x="dom-template-htmlFor">htmlFor</dfn>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we use htmlFor here? I thought that was no longer needed given changes to ECMAScript? We should use for.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTOH, do we really want the naming convention to be different for the same-named attribute on different HTML elements?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it depends on if we try to proceed with #9379 for the other cases.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think at this point it would be preferable to expose it as htmlFor and have aliases for all of them when we decide on #9379 rather than end up with a mix and match of for and htmlFor.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I don't feel super strongly myself, but web developers really seem to dislike the prefix so I'm somewhat hesitant to spread it further.

Copy link
Copy Markdown
Collaborator

@noamr noamr May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if @jakearchibald or @tunetheweb have a strong opinion about this. My opinion right now is that we should fix all of these together but I could be convinced that drawing the line here makes more sense.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinion myself but do agree that it seems weird to fix this one (and not support htmlFor at all?) while htmlFor is still the "standard" way IIUC?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggesting to resolve this comment for now. We are likely to get mixed opinions from developers. Let's create aliases for this together with #9379.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed this with @jakearchibald on #9379 as well. There is rough consensus on the plan of starting with htmlFor and aliasing all of those attributes to for (+ class) when we resolve on that.

Comment thread source Outdated
Comment thread source Outdated
Comment thread source
<th> <code data-x="">for</code>
<td> <code data-x="attr-template-for">template</code>
<td> Updates existing content
<td> Text
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we're missing an update to the template element index entry.

Comment thread source
<li><p>Let <var>nextNode</var> be <var>currentNode</var>'s
<span>next sibling</span>.</p></li>

<li><p><span data-x="concept-node-remove">Remove</span> <var>currentNode</var>.</p></li>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you remove them all at once? That's not a primitive that we have. How do we account for changes here?

Comment thread source Outdated
Comment thread source

<ol>
<li><p>If <var>sibling</var> is not a <code>ProcessingInstruction</code> node, then
<span>continue</span>.</p></li>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So now we are doing nested traversals. I'm not sure that actually works as the descendant iterator will also just pick the next one. Presumably we want to skip over the descendants we are handling here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not exactly "nested traversal". We reach the first matching PI and then sibling-traverse it, but after that we're done.
I double-checked this algorithm and I think it does the right thing... Can you be more specific if there is an issue here?

Comment thread source Outdated
Comment thread source
<p class="note">This is to support patching inside plain <code>template</code> elements or
those using <code data-x="attr-template-shadowrootmode">shadowrootmode</code>. Nested
patching is not supported, since in this case <var>scope</var>'s <span>template
contents</span> will be empty and <span>prepare content patching</span> will fail.</p>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Plain" template contents are a DocumentFragment. This means you have a bunch of type errors as you exclude that earlier.

Comment thread source
<p class="note">This is to support patching <code>head</code> with a <code>template</code>
inside <code>body</code>.</p>
</li>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have an assert here that scope is an Element or DocumentFragment.

Though also, can "the body element" be made to return null above with script?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the document doesn't have a body then the condition after "otherwise" is never true. I think we can assert that the scope is an element or document fragment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

Out of order HTML streaming ("patching")

6 participants