Skip to content

Conversation

@foolip
Copy link
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.)


/dom.html ( diff )
/indices.html ( diff )
/parsing.html ( diff )
/scripting.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
source Outdated

<li><p>Depending on the contentmethod state, remove existing nodes and update the insertion point.</p></li>

<li><p>Question: do we adjust the insertion point simply by setting <var>adjusted insertion location</var>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's a bit more involved because we need to adjust the location based on the grandparent's contentmethod and the parent's contentname.

source Outdated
<li><p>TODO: what to we need to do so that elements are inserted into the template element
and redirected by our pre-insertion steps? We need to undo whatever it is that causes nodes
to be inserted into the DocumentFragment normally.</p></li>
</ol>
Copy link
Collaborator

Choose a reason for hiding this comment

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

That's the "adjusted insertion location" or whatever it's called

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

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

4 participants