Add <template contentmethod> for declarative out-of-order streaming
Fixes https://github.com/whatwg/html/issues/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 )
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
contentnameattribute 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.
- Find the target element among the descendents of the element that the
- In appropriate place for inserting a node further adjust the location based on
contentmethodafter 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
contentnameattribute and the bookkeeping all checks out:- For
contentmethod=replace-childrenandcontentmethod=append, just append. - For
contentmethod=prepend, use the saved first element of the target.
- For
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.
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
contentnameattribute 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
contentmethodafter 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
contentnameattribute and the bookkeeping all checks out:
- For
contentmethod=replace-childrenandcontentmethod=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 thecontentnameattribute. 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.
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
headfrom withinheadis not possible but could easily be supported.
@noamr for the first one, you said we should fail, but what would that mean?
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
headfrom withinheadis 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.
Okay, should that state be sticky, or what happens if the nodes later realign so that the check passes?
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
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.
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.
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.