Add DocumentFragment, insertBefore, replaceChild, and createDocumentFragment with full JS binding support. Code review fixes: replace_child self-replacement now no-op per DOM spec, addEventListener validates DocumentFragment IDs, removed redundant dirty marking and double validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
16 KiB
Story 2.4: DOM Tree Mutation APIs
Status: done
Story
As a web developer using JavaScript, I want to create, insert, replace, and move DOM nodes programmatically, So that dynamic web pages can modify their structure at runtime.
Acceptance Criteria
-
document.createDocumentFragment()creates a lightweight container. When the fragment is inserted into the DOM viaappendChild,insertBefore, orreplaceChild, all child nodes transfer to the insertion point and the fragment becomes empty per DOM §4.5. -
parentNode.insertBefore(newNode, referenceNode)insertsnewNodebeforereferenceNodein the parent's child list. IfreferenceNodeisnull, appends to the end. IfnewNodealready exists in the DOM, it is moved. -
parentNode.replaceChild(newChild, oldChild)replacesoldChildwithnewChildat the same position in the parent's child list. The old child is removed and returned. IfnewChildalready exists in the DOM, it is moved. -
Node movement works correctly: inserting a node that already has a parent removes it from its current parent first (already works for
appendChild, must also work forinsertBeforeandreplaceChild). -
Integration tests verify each mutation API from JavaScript.
docs/HTML5_Implementation_Checklist.mdupdated.just cipasses.
Tasks / Subtasks
-
Task 1: Add DocumentFragment node type (AC: #1)
- 1.1 Add
NodeKind::DocumentFragmentvariant tocrates/dom/src/node.rs - 1.2 Add
Document::create_document_fragment(&mut self) -> NodeIdmethod indocument.rs - 1.3 Update all
matchonNodeKindacross the codebase to handle the new variant (search for exhaustive matches indom,html,style,layout,display_list,render,web_api) - 1.4 Implement fragment insertion semantics: when
append_child,insert_before, orreplace_childreceives aDocumentFragmentas the child, iterate its children and insert each one individually, then clear the fragment's children - 1.5 Unit tests: create fragment, add children, insert into DOM → children transfer, fragment empty
- 1.1 Add
-
Task 2: Expose insertBefore to JavaScript (AC: #2, #4)
- 2.1 Verify
Document::insert_before(parent, new_child, ref_child)exists from Story 2.3 — if not, implement it now incrates/dom/src/document.rs:- Remove
new_childfrom old parent if it has one - Find index of
ref_childinparent.children - Insert
new_childat that index - If
ref_childisNone, append to end - Set parent link, mark dirty
- Remove
- 2.2 Add JS binding in
crates/web_api/src/dom_host/host_environment.rs: wire"insertBefore"in thecall_methodmatch for"Element"type — extract parent_id, new_child_id, ref_child_id (handle JsValue::Null for ref_child), calldocument.insert_before(), return new_child as HostObject - 2.3 Handle DocumentFragment: if
new_childis a fragment, insert each fragment child beforeref_child - 2.4 Unit tests (Rust): insert before first child, middle child, last child; insert with null ref (append); move existing node; insert fragment
- 2.5 JS binding tests: verify insertBefore works from JavaScript
- 2.1 Verify
-
Task 3: Implement and expose replaceChild (AC: #3, #4)
- 3.1 Add
Document::replace_child(&mut self, parent: NodeId, new_child: NodeId, old_child: NodeId) -> Option<NodeId>indocument.rs:- Verify
old_childis a child ofparent, returnNoneif not - Remove
new_childfrom its current parent if it has one - Find index of
old_childinparent.children - Replace
old_childat that index withnew_child - Clear
old_child.parent, setnew_child.parent = Some(parent) - Mark dirty, return
Some(old_child)
- Verify
- 3.2 Add JS binding in
host_environment.rs: wire"replaceChild"incall_methodfor"Element"— extract parent_id, new_child_id, old_child_id, calldocument.replace_child(), return old_child as HostObject (or throw if old_child not found) - 3.3 Handle DocumentFragment: if
new_childis a fragment, replaceold_childwith all fragment children (first fragment child takes old_child's position, rest inserted after) - 3.4 Unit tests (Rust): replace first/middle/last child; replace with node from elsewhere; replace with fragment; error case (old_child not a child)
- 3.5 JS binding tests: verify replaceChild works from JavaScript
- 3.1 Add
-
Task 4: Wire createDocumentFragment to JavaScript (AC: #1)
- 4.1 Add JS binding in
host_environment.rs: wire"createDocumentFragment"incall_methodfor"Document"type — calldocument.create_document_fragment(), return asJsValue::HostObject { id, type_name: "DocumentFragment" } - 4.2 Ensure DocumentFragment objects support
appendChild,insertBefore,removeChild,replaceChildin the method dispatch (fragments act as parents) - 4.3 Ensure
textContentgetter/setter works on fragments - 4.4 JS binding tests: create fragment from JS, add children, insert into DOM
- 4.1 Add JS binding in
-
Task 5: Integration tests and documentation (AC: #5)
- 5.1 Add integration test: JS script that creates elements, uses insertBefore to reorder children, verifies DOM structure
- 5.2 Add integration test: JS script that creates fragment, adds elements, appends fragment, verifies children transferred
- 5.3 Add integration test: JS script that uses replaceChild, verifies old child removed and new child at correct position
- 5.4 Add integration test: JS script that moves a node (appendChild to different parent), verifies removed from old parent
- 5.5 Add golden test if any mutation affects rendering output
- 5.6 Update
docs/HTML5_Implementation_Checklist.md— check off DocumentFragment, insertBefore, replaceChild items - 5.7 Run
just ciand ensure all tests pass
Dev Notes
Dependencies on Previous Stories
Story 2.3 plans to add Document::insert_before() and Document::insert_at() as internal helpers for the parser's adoption agency algorithm. If Story 2.3 is complete:
insert_beforealready exists — just wire it to JS and add DocumentFragment handlinginsert_atmay also exist — useful forreplace_childimplementation
If Story 2.3 is NOT complete, implement insert_before here (Task 2.1).
Architecture: JS Binding Pattern
The DOM-to-JS bridge uses HostEnvironment trait in crates/web_api/src/dom_host/host_environment.rs:
// JS calls: element.appendChild(child)
// VM dispatches to:
fn call_method(&mut self, obj_id: u64, obj_type: &str, method: &str, args: &[JsValue])
-> Result<JsValue, RuntimeError>
// obj_id = NodeId.index() as u64
// obj_type = "Element", "Document", "DocumentFragment", etc.
// method = "appendChild", "insertBefore", etc.
// args = JS arguments as JsValue array
Existing method dispatch (in call_method):
"Document"+"getElementById"→document.get_element_by_id()"Document"+"createElement"→document.create_element()"Element"+"appendChild"→document.append_child()"Element"+"removeChild"→document.remove_child()
Add new dispatch entries:
"Document"+"createDocumentFragment"→document.create_document_fragment()"Element"+"insertBefore"→document.insert_before()"Element"+"replaceChild"→document.replace_child()"DocumentFragment"+ same methods as"Element"for tree mutation
DocumentFragment Semantics
Key behavior: fragment is a transparent container. When inserted:
// Pseudocode for append_child with fragment support:
fn append_child(&mut self, parent: NodeId, child: NodeId) {
if self.is_document_fragment(child) {
// Transfer each fragment child to parent
let fragment_children: Vec<NodeId> = self.children(child).to_vec();
for fc in fragment_children {
self.append_child(parent, fc); // recursive, but fc is never a fragment
}
// Fragment is now empty (children moved)
} else {
// Existing behavior: remove from old parent, append to new parent
// ...
}
}
Important: The existing append_child must be updated to handle fragments, not just the new methods. This means modifying an existing method — be careful not to break existing behavior.
NodeKind Match Exhaustiveness
Adding NodeKind::DocumentFragment will break all exhaustive match statements. Search the codebase for:
match.*node\.kind
match.*kind
NodeKind::
Key locations likely to need updates:
crates/dom/src/document.rs—inner_html(),text_content(), other methodscrates/html/src/tree_builder.rs— may match on NodeKindcrates/style/— style computation skips non-element nodescrates/layout/— layout computationcrates/display_list/— paint generationcrates/web_api/— property getters
For most cases, DocumentFragment should be handled like Document (transparent container) or ignored (no styling, no layout, no painting).
Error Handling for JS APIs
Follow the DOM spec's exception patterns:
insertBeforewith invalidref_child→ throwNotFoundError(returnErr(RuntimeError))replaceChildwithold_childnot a child of parent → throwNotFoundErrorreplaceChild/insertBeforewith node that would create a cycle → throwHierarchyRequestError
In the Rust binding, these map to returning Err(RuntimeError::new("NotFoundError: ...")).
What NOT to Change (Scope Boundaries)
- Do NOT add
querySelector/querySelectorAll— that's Story 2.5 - Do NOT add live collections (
getElementsByTagNamereturning live HTMLCollection) — that's Story 2.5 - Do NOT add
cloneNode— not in this story's scope - Do NOT add
innerHTMLsetter improvements — already works
Files to Modify
crates/dom/src/node.rs— addDocumentFragmenttoNodeKindcrates/dom/src/document.rs— addcreate_document_fragment(),replace_child(); verify/addinsert_before(); updateappend_child()for fragment semanticscrates/dom/src/lib.rs— ensure new types are exportedcrates/web_api/src/dom_host/host_environment.rs— wire JS bindings for all three new APIs + DocumentFragment method dispatchcrates/dom/src/tests/tree_ops_tests.rs— Rust-level unit testscrates/web_api/src/dom_host/tests/dom_tests.rs— JS binding tests- Various crates for
NodeKindmatch updates (style, layout, display_list, etc.) docs/HTML5_Implementation_Checklist.md— update checked items
Previous Story Intelligence
From Story 2.3:
Document::insert_before()andDocument::insert_at()may already be implemented as parser-internal helpers- These handle reparenting (remove from old parent first)
- Build on these rather than reimplementing
From Epic 1:
- Code review catches edge-case bugs — test fragment insertion thoroughly
- Golden tests essential for rendering changes
- Always update checklists
Testing Strategy
- Unit tests in
crates/dom/src/tests/tree_ops_tests.rs— Rust-level DOM operations - JS binding tests in
crates/web_api/src/dom_host/tests/dom_tests.rs— verify JS calls work - Integration tests — full pipeline: JS script → DOM mutation → verify tree structure
- Key test scenarios:
insertBefore(newNode, firstChild)— prependinsertBefore(newNode, null)— appendinsertBefore(existingNode, refNode)— movereplaceChild(newNode, oldNode)— swapreplaceChild(existingNode, oldNode)— move + swap- Fragment with 3 children →
appendChild(fragment)→ 3 children transferred - Fragment with children →
insertBefore(fragment, refNode)→ children inserted before ref - Empty fragment →
appendChild(fragment)→ no-op - Error:
insertBefore(node, nonChildRef)→ throws
References
- DOM Living Standard §4.4 — Node interface
- DOM Living Standard §4.5 — DocumentFragment interface
- [Source: crates/dom/src/document.rs] — existing DOM operations
- [Source: crates/dom/src/node.rs] — Node struct, NodeKind enum
- [Source: crates/web_api/src/dom_host/host_environment.rs] — JS binding dispatch
- [Source: crates/web_api/src/dom_host/tests/dom_tests.rs] — existing JS binding tests
- [Source: crates/dom/src/tests/tree_ops_tests.rs] — existing DOM unit tests
- [Source: docs/HTML5_Implementation_Checklist.md] — checklist to update
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
No HALT conditions encountered. All tasks completed in a single implementation pass.
Completion Notes List
- Task 1: Added
NodeKind::DocumentFragmentvariant. Createdcreate_document_fragment()andis_document_fragment()methods. Updated exhaustive matches inserialize_node(combined with Document arm) anddump_node(new arm). Updatedset_text_contentto handle fragments like elements. Refactoredappend_childandinsert_beforeto delegate to internal_singlemethods, with fragment path that transfers children. Addedreplace_childwith full fragment support. 7 unit tests added. - Task 2:
insert_beforealready existed from Story 2.3. Wired JS binding with null ref_child handling (appends) and NotFoundError for invalid ref_child. Fragment support inherited from DOM layer. 3 unit tests + 3 integration tests added. - Task 3: Implemented
Document::replace_child()with sibling-aware position tracking, including self-replacement no-op per DOM spec. Wired JS binding with NotFoundError on invalid old_child. Fragment support handles multi-child replacement at correct position. 4 unit tests + 2 integration tests added. - Task 4: Wired
createDocumentFragmenton Document type. AddedDocumentFragmenttype dispatch incall_method(delegates to Element handlers),get_property, andset_property(textContent support). 4 unit tests + 2 integration tests added. - Task 5: Added 9 integration tests in
tests/js_dom_tests.rs(including fragment replaceChild). No golden test needed (mutations don't affect rendering output since fragments are never rendered). Updated HTML5 checklist.just cipasses with 0 failures.
File List
crates/dom/src/node.rs— AddedDocumentFragmentvariant toNodeKindcrates/dom/src/document.rs— Addedcreate_document_fragment(),is_document_fragment(),replace_child(); refactoredappend_child/insert_beforefor fragment semantics; updated exhaustive matches inserialize_node,dump_node,set_text_contentcrates/dom/src/tests/tree_ops_tests.rs— Added 12 unit tests for fragment, insertBefore, replaceChild (including self-replacement no-op)crates/web_api/src/dom_host/host_environment.rs— AddedcreateDocumentFragment,insertBefore,replaceChildbindings; addedDocumentFragmenttype dispatch; updated get/set_property for DocumentFragment textContentcrates/web_api/src/dom_host/tests/dom_tests.rs— Added 9 unit tests for JS bindingstests/js_dom_tests.rs— Added 9 integration tests for insertBefore, replaceChild, createDocumentFragment, node movement, fragment replaceChilddocs/HTML5_Implementation_Checklist.md— Checked off DOM tree node types and DOM mutation algorithms
Change Log
- 2026-03-14: Implemented Story 2.4 — DOM Tree Mutation APIs (DocumentFragment, insertBefore, replaceChild, createDocumentFragment) with full JS binding support and comprehensive tests
- 2026-03-14: Code review fixes — (1) replace_child self-replacement now no-op per DOM spec, (2) addEventListener validates DocumentFragment IDs, (3) removed redundant dirty marking in append_child fragment path, (4) removed double validate_element_id in DocumentFragment delegation, (5) added missing fragment replaceChild integration test, (6) corrected test counts in Dev Agent Record