Create comprehensive implementation-ready story files for the remaining Epic 3 (JavaScript Engine Maturity) stories and update sprint status from backlog to ready-for-dev: - 3.5: Built-in Completeness (Array/String/Object) - 3.6: Built-in Completeness (Date/RegExp/Map/Set) - 3.7: WeakRef, FinalizationRegistry & Strict Mode Edge Cases - 3.8: DOM Bindings via web_api - 3.9: Event Dispatch Completeness - 3.10: Web API Exposure (fetch, Math, setInterval, rAF) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
23 KiB
Story 3.8: DOM Bindings via web_api
Status: ready-for-dev
Story
As a web developer using JavaScript, I want full DOM manipulation capabilities from JavaScript, So that dynamic web pages can create, modify, and style elements at runtime.
Acceptance Criteria
-
Attribute access:
element.getAttribute(name)returns attribute value or null.element.setAttribute(name, value)sets attribute and triggers dependent behavior (e.g.,classattribute updates styling).element.removeAttribute(name)removes attribute.element.hasAttribute(name)returns boolean. Per DOM §7.3. -
Element identity properties:
element.id(get/set) reflects theidattribute.element.className(get/set) reflects theclassattribute.element.tagNamereturns uppercase tag name for HTML elements. Per DOM §5.4/§7.3. -
classList DOMTokenList:
element.classList.add(...classes),.remove(...classes),.toggle(class, force?),.contains(class),.replace(old, new),.length,.item(index),[index]access. Modifying classList updates theclassattribute and triggers style recomputation. Per DOM §7.1. -
Inline style access:
element.style.propertyName(get/set) for camelCase CSS properties (e.g.,element.style.backgroundColor = "red").element.style.cssText(get/set) for full inline style string.element.style.getPropertyValue(name)andelement.style.setProperty(name, value)for kebab-case access. Setting style triggers re-style/re-layout on next render. Per CSSOM §6.7. -
Tree navigation properties:
node.parentNode,node.parentElement,node.childNodes(live NodeList),node.children(live HTMLCollection of elements only),node.firstChild,node.lastChild,node.nextSibling,node.previousSibling,node.firstElementChild,node.lastElementChild,node.nextElementSibling,node.previousElementSibling. All return HostObject or null. Per DOM §4.4. -
Additional element properties:
element.outerHTML(getter),element.cloneNode(deep),element.contains(other),element.matches(selector),element.closest(selector),node.nodeType,node.nodeName,node.ownerDocument,element.childElementCount. Per DOM §4.4/§4.5. -
Integration tests verify each binding, DOM checklist is updated, and
just cipasses.
What NOT to Implement
- No
element.dataset--data-*attribute reflection deferred.getAttribute("data-foo")works as workaround. - No
NamedNodeMap--element.attributescollection deferred. IndividualgetAttribute/setAttributesufficient. - No computed style --
window.getComputedStyle(element)deferred to Story 3.10 (Web API Exposure). - No
element.scrollTop/scrollLeft/scrollWidth/scrollHeight-- scroll metrics deferred to Epic 5. - No
element.offsetTop/offsetLeft/offsetWidth/offsetHeight/getBoundingClientRect()-- layout metrics deferred to Story 3.10. - No
element.focus()/element.blur()-- focus management deferred to Epic 4. - No
MutationObserver-- DOM observation API deferred. - No
element.animate()-- Web Animations API out of scope.
Files to Modify
| File | Change |
|---|---|
crates/web_api/src/dom_host/host_environment.rs |
Add attribute access (getAttribute/setAttribute/removeAttribute/hasAttribute), tree navigation properties (parentNode, childNodes, nextSibling, etc.), element identity (id, className, tagName), classList dispatch, style dispatch, cloneNode, contains, matches, closest, nodeType, nodeName, outerHTML. |
crates/web_api/src/dom_host/mod.rs |
Add classList helper struct/methods. Add CSSStyleDeclaration helper for element.style bridge. |
crates/web_api/src/dom_host/classlist.rs |
New file -- DOMTokenList implementation for classList. |
crates/web_api/src/dom_host/style_bridge.rs |
New file -- CSSStyleDeclaration bridge: camelCase-to-kebab conversion, inline style parsing/serialization, property get/set. |
crates/dom/src/document.rs |
Expose any missing DOM operations needed (most already exist). Add clone_node(id, deep) if not present. Add contains(ancestor_id, descendant_id). |
crates/web_api/src/dom_host/live_collection.rs |
Add NodeList support (childNodes returns live NodeList of all node types, not just elements). |
crates/web_api/src/dom_host/tests/dom_tests.rs |
Add tests for all new bindings. |
tests/js_dom_tests.rs |
Add integration tests for attribute access, classList, style, navigation, cloneNode, matches, closest. |
docs/HTML5_Implementation_Checklist.md |
Check off DOMTokenList, attribute access, navigation properties. |
Tasks / Subtasks
-
Task 1: Attribute access APIs (AC: #1)
- 1.1 Add
getAttribute,setAttribute,removeAttribute,hasAttributetocall_method()inhost_environment.rs:getAttribute(name)→ delegate todocument.get_attribute(node_id, &name), return String or NullsetAttribute(name, value)→ delegate todocument.set_attribute(node_id, &name, &value), return UndefinedremoveAttribute(name)→ delegate todocument.remove_attribute(node_id, &name)(add to dom crate if missing), return UndefinedhasAttribute(name)→ check if attribute exists, return Boolean- All methods: validate that host object is Element type, TypeError otherwise
- 1.2 Add
remove_attribute(node_id, name)tocrates/dom/src/document.rsif not already present - 1.3 Add unit tests and integration tests:
element.setAttribute("data-x", "hello"); element.getAttribute("data-x") === "hello"element.removeAttribute("class"); element.hasAttribute("class") === false- Setting
classattribute manually should work (classList tests in Task 3)
- 1.1 Add
-
Task 2: Element identity and navigation properties (AC: #2, #5)
- 2.1 Add element identity properties to
get_property()inhost_environment.rs:"id"→document.get_attribute(node_id, "id")or empty string"className"→document.get_attribute(node_id, "class")or empty string"tagName"→document.tag_name(node_id).to_uppercase()(HTML elements return uppercase per spec)"nodeName"→ same as tagName for elements;"#text"for text nodes,"#document"for document,"#comment"for comments,"#document-fragment"for fragments"nodeType"→1(Element),3(Text),8(Comment),9(Document),11(DocumentFragment)"ownerDocument"→ return Document HostObject (DOCUMENT_HOST_ID)"childElementCount"→ count of element children
- 2.2 Add
idandclassNamesetters toset_property():"id"→document.set_attribute(node_id, "id", &value)"className"→document.set_attribute(node_id, "class", &value)
- 2.3 Add tree navigation properties to
get_property():"parentNode"→document.parent(node_id)→ HostObject or Null"parentElement"→document.parent_element(node_id)→ HostObject or Null (only if parent is element)"firstChild"→document.first_child(node_id)→ HostObject or Null"lastChild"→document.last_child(node_id)→ HostObject or Null (add to dom if missing)"nextSibling"→document.next_sibling(node_id)→ HostObject or Null"previousSibling"→document.previous_sibling(node_id)→ HostObject or Null (add to dom if missing)"firstElementChild"→ first child that is an element → HostObject or Null"lastElementChild"→ last child that is an element → HostObject or Null"nextElementSibling"→ next sibling that is an element → HostObject or Null"previousElementSibling"→ previous sibling that is an element → HostObject or Null
- 2.4 Add
childNodesproperty (live NodeList):- Return a HostObject representing a live NodeList (includes ALL child nodes: elements, text, comments)
- Reuse
LiveCollectionpattern fromlive_collection.rsbut for all node types - Support
length,[index],item(index)access
- 2.5 Add
childrenproperty (live HTMLCollection):- Return a HostObject representing a live HTMLCollection (elements only)
- Reuse existing
LiveCollectioninfrastructure
- 2.6 Add missing DOM helpers to
crates/dom/src/document.rs:last_child(id)if not presentprevious_sibling(id)if not present- Helper iterators for element-only navigation
- 2.7 Add tests for all navigation properties and identity getters
- 2.1 Add element identity properties to
-
Task 3: classList DOMTokenList (AC: #3)
- 3.1 Create
crates/web_api/src/dom_host/classlist.rs:ClassListis NOT a standalone object -- it's a live view of the element'sclassattribute- When JS accesses
element.classList, return a HostObject with type"DOMTokenList"and a combined ID encoding both the element NodeId and a marker - ID encoding:
CLASSLIST_ID_BASE + node_id(reserve ID range like LiveCollection)
- 3.2 Implement DOMTokenList methods via
call_method()dispatch on"DOMTokenList"type:.add(...classes)→ parse class attribute, add new classes, set attribute back.remove(...classes)→ parse, remove, set back.toggle(class, force?)→ toggle or force add/remove, return boolean.contains(class)→ check if class exists in attribute.replace(oldClass, newClass)→ replace if present, return boolean.item(index)→ return class at index or null.length(via get_property) → number of classes- Index access (
0,1, etc.) → same as item()
- 3.3 Ensure class attribute changes trigger style recomputation:
document.set_attribute()forclassshould be sufficient if the rendering pipeline re-matches selectors- Verify: After
classList.add("highlight"), next render cycle applies.highlightstyles
- 3.4 Add
"classList"toget_property()for Element type → return DOMTokenList HostObject - 3.5 Add tests:
el.classList.add("a", "b"); el.className === "a b"el.classList.remove("a"); el.className === "b"el.classList.toggle("c") === true; el.classList.contains("c") === trueel.classList.toggle("c") === false; el.classList.contains("c") === falseel.classList.replace("b", "d")el.classList.length === 1
- 3.1 Create
-
Task 4: Inline style access (AC: #4)
- 4.1 Create
crates/web_api/src/dom_host/style_bridge.rs:CSSStyleDeclarationbridge between JSelement.style.backgroundColorand DOM inlinestyleattribute- camelCase to kebab-case conversion:
backgroundColor→background-color,marginTop→margin-top - kebab-case to camelCase for reading
- Handle CSS vendor prefixes:
webkitTransform→-webkit-transform(low priority)
- 4.2 Return CSSStyleDeclaration HostObject from
get_property("style"):- Use ID encoding:
STYLE_ID_BASE + node_id - Type name:
"CSSStyleDeclaration"
- Use ID encoding:
- 4.3 Implement style property get/set via
get_property()/set_property()on"CSSStyleDeclaration"type:- Get: Parse element's
styleattribute, find matching property, return value string or empty string - Set: Parse existing style attribute, update/add property, serialize back to attribute
cssText(get): Return fullstyleattribute valuecssText(set): Replace entirestyleattribute
- Get: Parse element's
- 4.4 Implement
getPropertyValue(name)andsetProperty(name, value)viacall_method():- Accept kebab-case property names (
"background-color") removeProperty(name)removes the property and returns old value
- Accept kebab-case property names (
- 4.5 Inline style parsing/serialization:
- Parse:
"color: red; font-size: 16px"→Vec<(String, String)> - Serialize:
Vec<(String, String)>→"color: red; font-size: 16px" - Reuse CSS parser? The
csscrate can parse property values. For inline styles, a simple split on;and:may suffice.
- Parse:
- 4.6 Ensure style changes trigger re-render:
- Setting
styleattribute viadocument.set_attribute()should invalidate style computation
- Setting
- 4.7 Add tests:
el.style.color = "red"; el.getAttribute("style")includescolor: redel.style.backgroundColor = "blue"→el.getAttribute("style")includesbackground-color: blueel.style.cssText = "margin: 10px"replaces all inline stylesel.style.getPropertyValue("color") === "red"el.style.removeProperty("color")removes it
- 4.1 Create
-
Task 5: Additional element methods (AC: #6)
- 5.1 Implement
element.cloneNode(deep)incall_method():- Shallow clone (
deep=false): clone element and attributes, no children - Deep clone (
deep=true): recursively clone element, attributes, and all descendants - Add
clone_node(node_id, deep)tocrates/dom/src/document.rs - Return new HostObject for cloned node
- Shallow clone (
- 5.2 Implement
element.contains(other)incall_method():- Walk ancestors of
otherchecking if any matchelement - Return boolean.
element.contains(element)returns true (contains itself). - Add
contains(ancestor_id, descendant_id)to dom crate if missing
- Walk ancestors of
- 5.3 Implement
element.matches(selector)incall_method():- Parse selector string using
selectorscrate - Check if element matches the selector
- Return boolean
- Reuse existing selector matching from
document.query_selector()path
- Parse selector string using
- 5.4 Implement
element.closest(selector)incall_method():- Walk ancestors (including self) checking
matches(selector)on each - Return first matching ancestor as HostObject, or Null
- Walk ancestors (including self) checking
- 5.5 Implement
element.outerHTMLgetter inget_property():- Serialize element AND its content to HTML string
- Add
outer_html(node_id)to dom crate (wraps inner_html with the element's own tag)
- 5.6 Add tests for cloneNode, contains, matches, closest, outerHTML
- 5.1 Implement
-
Task 6: Testing and validation (AC: #7)
- 6.1 Add integration tests in
tests/js_dom_tests.rs:- Attribute access: get/set/remove/has
- Identity properties: id, className, tagName
- classList: add, remove, toggle, contains, replace
- Style access: camelCase property set/get, cssText, getPropertyValue/setProperty
- Navigation: parentNode, childNodes, firstChild, lastChild, nextSibling, previousSibling
- Element navigation: firstElementChild, nextElementSibling, children
- Methods: cloneNode (shallow and deep), contains, matches, closest
- nodeType, nodeName for different node types
- 6.2 Run all existing test suites to verify no regressions:
cargo test -p web_apicargo test -p domcargo test -p rust_browser --test js_dom_testscargo test -p rust_browser --test js_testscargo test -p rust_browser --test js_eventscargo test -p rust_browser --test goldens
- 6.3 Update
docs/HTML5_Implementation_Checklist.md:- Check off DOMTokenList (classList)
- Check off attribute access APIs
- Check off navigation properties
- Check off reflecting IDL attributes (id, className)
- 6.4 Run
just ci-- full validation pass
- 6.1 Add integration tests in
Dev Notes
Key Architecture Decisions
All bindings go through HostEnvironment trait. JS code accesses DOM via JsValue::HostObject { id, type_name }. The DomHost implementation of get_property(), set_property(), and call_method() dispatches based on type_name ("Element", "Document", "DOMTokenList", "CSSStyleDeclaration", "HTMLCollection", "NodeList"). No direct DOM access from JS engine.
classList and style return sub-HostObjects. element.classList returns HostObject { id: CLASSLIST_ID_BASE + node_id, type_name: "DOMTokenList" }. element.style returns HostObject { id: STYLE_ID_BASE + node_id, type_name: "CSSStyleDeclaration" }. These are live views -- they read/write the underlying DOM attribute each time.
ID space allocation pattern (existing):
u64::MAX — DOCUMENT_HOST_ID
u64::MAX-2 — WINDOW_HOST_ID
u64::MAX-1000 ↓ — EVENT_HOST_ID_BASE
u64::MAX-100_000 ↓ — PROMISE_ID_START
u64::MAX-200_000 ↓ — COLLECTION_ID_BASE
0 ↑ — DOM NodeIds (elements, text nodes)
New ranges needed:
u64::MAX-300_000 ↓ — CLASSLIST_ID_BASE (classList objects)
u64::MAX-400_000 ↓ — STYLE_ID_BASE (style objects)
u64::MAX-500_000 ↓ — NODELIST_ID_BASE (childNodes NodeLists)
Inline style parsing is simple string manipulation. Don't use the full CSS parser for element.style access. Split on ;, then : for each property. Serialize back with proper spacing. The full CSS parser is overkill for inline style get/set.
Implementation Patterns from Existing Code
Property getter dispatch (in host_environment.rs:get_property()):
match obj_type {
"Element" | "DocumentFragment" => match property {
"textContent" => { /* ... */ }
"innerHTML" => { /* ... */ }
// Add: "id", "className", "tagName", "parentNode", "childNodes", etc.
},
"Document" => match property {
"readyState" => { /* ... */ }
// ...
},
// Add: "DOMTokenList", "CSSStyleDeclaration", "NodeList"
}
Method call dispatch (in host_environment.rs:call_method()):
match obj_type {
"Element" | "DocumentFragment" => match method {
"appendChild" => { /* ... */ }
// Add: "getAttribute", "setAttribute", "cloneNode", "matches", "closest"
},
}
HostObject creation:
Ok(JsValue::HostObject {
id: node_id.index() as u64,
type_name: "Element".to_string(),
})
Critical Implementation Details
element.id setter must update the document's ID index. When element.id = "newId", the DOM's get_element_by_id() lookup must reflect the change. Verify that document.set_attribute(node_id, "id", value) updates the internal index.
classList mutations must update the class attribute. After classList.add("foo"), getAttribute("class") must include "foo". Implement classList by reading the current class attribute value, tokenizing on whitespace, mutating the token list, and writing back.
camelCase-to-kebab conversion for style properties:
fn camel_to_kebab(name: &str) -> String {
// "backgroundColor" → "background-color"
// "marginTop" → "margin-top"
// "cssFloat" → "float" (special case)
// "WebkitTransform" → "-webkit-transform" (if starts with uppercase)
}
childNodes returns ALL children (text, comment, element). This is different from children which returns only elements. The existing LiveCollection infrastructure may need adaptation since it currently only handles elements.
cloneNode(deep) must NOT copy event listeners. Per spec, cloned nodes don't carry over addEventListener registrations.
matches(selector) reuse. The selectors crate already has a matches() function used by querySelector. Reuse the same selector compilation and matching logic.
Dependency on Other Stories
No hard dependencies. This story builds on existing web_api infrastructure. The property descriptor system (Story 3.5) is not required -- inline style and attribute access use the HostEnvironment bridge, not JS property descriptors.
Story 3.9 (Event Dispatch Completeness) builds on this story's DOM navigation (e.g., building ancestor chains for capture/bubble phases). Tree navigation properties from Task 2 will be used by event dispatch.
Previous Story Patterns
From Story 3.4 (ES modules) and 3.7 (strict mode):
- HostEnvironment methods already have extensive match arms -- adding more follows the established pattern
- All new host object types need ID range reservation to avoid collisions
- Integration tests in
tests/js_dom_tests.rsfollow existing patterns - Run
just ciafter each task
Risk Assessment
MEDIUM: host_environment.rs file size. This file is already 1100+ lines. Adding attribute access, navigation, classList, and style dispatch will make it larger. Consider extracting DOMTokenList and CSSStyleDeclaration dispatch into separate files imported by host_environment.
MEDIUM: Inline style parsing correctness. CSS property values can contain colons and semicolons (e.g., background: url("data:image/png;base64,...") ). Simple string splitting may break on edge cases. Use a more robust parser that respects quotes and parentheses.
LOW: Tree navigation properties. Most DOM navigation methods already exist in the dom crate. Just need to wire them through HostEnvironment.
LOW: Attribute access. Direct delegation to existing dom crate methods.
Phased Implementation Strategy
Phase A -- Attributes + Identity (Tasks 1-2): Foundation. Wire existing DOM methods through HostEnvironment. Quick wins.
Phase B -- classList (Task 3): New sub-HostObject type with DOMTokenList protocol. Moderate complexity.
Phase C -- Style Bridge (Task 4): New sub-HostObject type with camelCase conversion. Moderate complexity.
Phase D -- Additional Methods (Task 5): cloneNode, contains, matches, closest. Each is standalone.
Phase E -- Testing + Validation (Task 6): After all bindings implemented.
Project Structure Notes
- All DOM binding changes in
crates/web_api/src/dom_host/(Layer 1) -- no layer violations - DOM operations in
crates/dom/src/(Layer 1) -- may need minor additions - New files:
classlist.rs,style_bridge.rsincrates/web_api/src/dom_host/ web_apidepends ondom,css,selectors(all Layer 1, horizontal deps OK)- No
unsafecode needed - No new external dependencies
References
- DOM Living Standard §4.4 -- Interface Node -- parentNode, childNodes, firstChild, etc.
- DOM Living Standard §4.5 -- Interface Element -- getAttribute, setAttribute, matches, closest
- DOM Living Standard §7.1 -- DOMTokenList -- classList interface
- DOM Living Standard §7.3 -- NamedNodeMap -- Attribute access
- CSSOM §6.7 -- CSSStyleDeclaration -- Inline style access
- [Source: crates/web_api/src/dom_host/host_environment.rs] -- Existing HostEnvironment impl (1100+ lines)
- [Source: crates/web_api/src/dom_host/mod.rs] -- DomHost struct
- [Source: crates/web_api/src/dom_host/live_collection.rs] -- LiveCollection pattern for HTMLCollection
- [Source: crates/dom/src/document.rs] -- DOM operations (get_attribute, set_attribute, parent, children, etc.)
- [Source: crates/web_api/src/lib.rs] -- WebApiFacade with ID space allocation constants
- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.8] -- Story requirements
- [Source: _bmad-output/planning-artifacts/architecture.md#Web API Crate Scaling] -- Module-per-domain organization
Dev Agent Record
Agent Model Used
{{agent_model_name_version}}