fix(dom): bridge <template> contents to JS so .content is not always empty (#463)
The parser already put template children in a separate contents document
per the HTML spec (tree_sink.rs), but nothing exposed that node, so
`get content()` fabricated an empty DocumentFragment and the parsed
markup was unreachable:
document.body.innerHTML = '<template id=t><p>a</p></template>';
t.content.childNodes.length // 0 (Chrome: 1)
t.innerHTML // "" (Chrome: '<p>a</p>')
Every <template>-driven UI — custom elements cloning
`template.content`, lit, Vue runtime templates, htmx fragments, Alpine
x-if/x-for — stamped nothing, with no error raised.
Three layers were involved:
- `DomTree::template_contents` returns the contents node, allocating one
on demand so templates built via `createElement` work too. Exposed as
the `template_contents` op; `get content()` wraps it as a
DocumentFragment cached by node id, so `.content` keeps a stable
identity across reads.
- `import_node_from` cloned the element data verbatim, carrying the
*source* tree's contents NodeId into the destination tree, where it
indexed whatever unrelated node occupied that slot. That is why a
template injected via innerHTML resolved `.content` to a bogus node
rather than merely an empty one. The contents are now imported into a
fresh node and the reference remapped, queued onto the existing
worklist so nested templates stay iterative.
- The serializer emitted a template's own (always empty) children, so
contents vanished from outerHTML/innerHTML and `cloneNode(true)` —
which round-trips through outer_html — produced an empty template.
`content_source` resolves a template to its contents document for both
inner_html and outer_html.
With serialization fixed on the Rust side, the `set innerHTML` template
branch that routed to `.content` now has a matching getter for free, so
no template special case is needed on the JS hot path.
This commit is contained in:
1 parent
485497393d
commit
ae438e196b
5 files changed
+231
-2
No files matched your search
@@ -21,10 +21,21 @@ impl DomTree {
|
||||
|
||||
pub fn inner_html(&self, node_id: NodeId) -> String {
|
||||
let mut buf = String::new();
|
||||
self.serialize_worklist(self.child_work(node_id), &mut buf);
|
||||
self.serialize_worklist(self.child_work(self.content_source(node_id)), &mut buf);
|
||||
buf
|
||||
}
|
||||
|
||||
// The node whose children represent `node_id`'s markup. For a <template>
|
||||
// that is its contents document, since the parser puts template children
|
||||
// there rather than under the element (issue #463); for everything else it
|
||||
// is the node itself.
|
||||
fn content_source(&self, node_id: NodeId) -> NodeId {
|
||||
match self.get_node(node_id).map(|n| n.data) {
|
||||
Some(NodeData::Element { template_contents: Some(contents), .. }) => contents,
|
||||
_ => node_id,
|
||||
}
|
||||
}
|
||||
|
||||
// Children as work items in document order (top of the LIFO stack first).
|
||||
fn child_work(&self, node_id: NodeId) -> Vec<SerializeWork> {
|
||||
self.children(node_id)
|
||||
@@ -101,7 +112,9 @@ impl DomTree {
|
||||
if include_self {
|
||||
stack.push(SerializeWork::CloseTag(tag.to_string()));
|
||||
}
|
||||
for w in self.child_work(node_id) {
|
||||
// A <template> serializes its contents document, not its
|
||||
// own (always empty) children — issue #463.
|
||||
for w in self.child_work(self.content_source(node_id)) {
|
||||
stack.push(w);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,6 +603,41 @@ impl DomTree {
|
||||
None
|
||||
}
|
||||
|
||||
/// The node holding a `<template>` element's contents.
|
||||
///
|
||||
/// The parser puts template children in a separate contents document rather
|
||||
/// than under the element (HTML spec), so this is the only way to reach
|
||||
/// them. Templates built with `createElement` have no contents node yet, so
|
||||
/// one is allocated on demand — `.content` must be usable either way.
|
||||
///
|
||||
/// Returns `None` for a non-element node.
|
||||
pub fn template_contents(&self, node_id: NodeId) -> Option<NodeId> {
|
||||
{
|
||||
let inner = self.inner.borrow();
|
||||
let node = inner.nodes.get(node_id.index())?.as_ref()?;
|
||||
match &node.data {
|
||||
NodeData::Element { template_contents, .. } => {
|
||||
if let Some(existing) = *template_contents {
|
||||
return Some(existing);
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
// Borrow released above: `new_node` takes its own mutable borrow.
|
||||
// Matches what the tree sink allocates for a parsed template.
|
||||
let contents = self.new_node(NodeData::Document);
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if let Some(Some(node)) = inner.nodes.get_mut(node_id.index()) {
|
||||
if let NodeData::Element { template_contents, .. } = &mut node.data {
|
||||
*template_contents = Some(contents);
|
||||
return Some(contents);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn ancestors(&self, node_id: NodeId) -> Vec<NodeId> {
|
||||
let inner = self.inner.borrow();
|
||||
let mut result = Vec::new();
|
||||
@@ -727,6 +762,38 @@ impl DomTree {
|
||||
let new_id = self.new_node(node_data);
|
||||
self.append_child(dest_parent, new_id);
|
||||
|
||||
// A <template>'s children hang off a separate contents document, so
|
||||
// the child walk below never reaches them. Worse, the cloned data
|
||||
// carries the *source* tree's contents NodeId, which here indexes
|
||||
// whatever unrelated node occupies that slot. Allocate a real
|
||||
// contents node and queue the source contents' children into it, so
|
||||
// the reference is remapped rather than left dangling (issue #463).
|
||||
let src_contents = {
|
||||
let inner = self.inner.borrow();
|
||||
match inner.nodes.get(new_id.index()).and_then(|n| n.as_ref()) {
|
||||
Some(node) => match &node.data {
|
||||
NodeData::Element { template_contents, .. } => *template_contents,
|
||||
_ => None,
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
if let Some(src_contents) = src_contents {
|
||||
let dest_contents = self.new_node(NodeData::Document);
|
||||
{
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if let Some(Some(node)) = inner.nodes.get_mut(new_id.index()) {
|
||||
if let NodeData::Element { template_contents, .. } = &mut node.data {
|
||||
*template_contents = Some(dest_contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Onto the same stack, so nested templates stay iterative.
|
||||
for child_id in source.children(src_contents).into_iter().rev() {
|
||||
stack.push((dest_contents, child_id));
|
||||
}
|
||||
}
|
||||
|
||||
for child_id in source.children(src_id).into_iter().rev() {
|
||||
stack.push((new_id, child_id));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user