Files
rust_browser/_bmad-output/implementation-artifacts/3-8-dom-bindings-via-web-api.md
Zachary D. Rowitsch fb64ca1d34
All checks were successful
ci / fast (linux) (push) Successful in 7m9s
Create story files for Epic 3 stories 3.5-3.10
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>
2026-03-16 08:06:03 -04:00

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

  1. Attribute access: element.getAttribute(name) returns attribute value or null. element.setAttribute(name, value) sets attribute and triggers dependent behavior (e.g., class attribute updates styling). element.removeAttribute(name) removes attribute. element.hasAttribute(name) returns boolean. Per DOM §7.3.

  2. Element identity properties: element.id (get/set) reflects the id attribute. element.className (get/set) reflects the class attribute. element.tagName returns uppercase tag name for HTML elements. Per DOM §5.4/§7.3.

  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 the class attribute and triggers style recomputation. Per DOM §7.1.

  4. 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) and element.style.setProperty(name, value) for kebab-case access. Setting style triggers re-style/re-layout on next render. Per CSSOM §6.7.

  5. 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.

  6. 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.

  7. Integration tests verify each binding, DOM checklist is updated, and just ci passes.

What NOT to Implement

  • No element.dataset -- data-* attribute reflection deferred. getAttribute("data-foo") works as workaround.
  • No NamedNodeMap -- element.attributes collection deferred. Individual getAttribute/setAttribute sufficient.
  • 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, hasAttribute to call_method() in host_environment.rs:
      • getAttribute(name) → delegate to document.get_attribute(node_id, &name), return String or Null
      • setAttribute(name, value) → delegate to document.set_attribute(node_id, &name, &value), return Undefined
      • removeAttribute(name) → delegate to document.remove_attribute(node_id, &name) (add to dom crate if missing), return Undefined
      • hasAttribute(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) to crates/dom/src/document.rs if 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 class attribute manually should work (classList tests in Task 3)
  • Task 2: Element identity and navigation properties (AC: #2, #5)

    • 2.1 Add element identity properties to get_property() in host_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 id and className setters to set_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 childNodes property (live NodeList):
      • Return a HostObject representing a live NodeList (includes ALL child nodes: elements, text, comments)
      • Reuse LiveCollection pattern from live_collection.rs but for all node types
      • Support length, [index], item(index) access
    • 2.5 Add children property (live HTMLCollection):
      • Return a HostObject representing a live HTMLCollection (elements only)
      • Reuse existing LiveCollection infrastructure
    • 2.6 Add missing DOM helpers to crates/dom/src/document.rs:
      • last_child(id) if not present
      • previous_sibling(id) if not present
      • Helper iterators for element-only navigation
    • 2.7 Add tests for all navigation properties and identity getters
  • Task 3: classList DOMTokenList (AC: #3)

    • 3.1 Create crates/web_api/src/dom_host/classlist.rs:
      • ClassList is NOT a standalone object -- it's a live view of the element's class attribute
      • 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() for class should be sufficient if the rendering pipeline re-matches selectors
      • Verify: After classList.add("highlight"), next render cycle applies .highlight styles
    • 3.4 Add "classList" to get_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") === true
      • el.classList.toggle("c") === false; el.classList.contains("c") === false
      • el.classList.replace("b", "d")
      • el.classList.length === 1
  • Task 4: Inline style access (AC: #4)

    • 4.1 Create crates/web_api/src/dom_host/style_bridge.rs:
      • CSSStyleDeclaration bridge between JS element.style.backgroundColor and DOM inline style attribute
      • camelCase to kebab-case conversion: backgroundColorbackground-color, marginTopmargin-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"
    • 4.3 Implement style property get/set via get_property()/set_property() on "CSSStyleDeclaration" type:
      • Get: Parse element's style attribute, find matching property, return value string or empty string
      • Set: Parse existing style attribute, update/add property, serialize back to attribute
      • cssText (get): Return full style attribute value
      • cssText (set): Replace entire style attribute
    • 4.4 Implement getPropertyValue(name) and setProperty(name, value) via call_method():
      • Accept kebab-case property names ("background-color")
      • removeProperty(name) removes the property and returns old value
    • 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 css crate can parse property values. For inline styles, a simple split on ; and : may suffice.
    • 4.6 Ensure style changes trigger re-render:
      • Setting style attribute via document.set_attribute() should invalidate style computation
    • 4.7 Add tests:
      • el.style.color = "red"; el.getAttribute("style") includes color: red
      • el.style.backgroundColor = "blue"el.getAttribute("style") includes background-color: blue
      • el.style.cssText = "margin: 10px" replaces all inline styles
      • el.style.getPropertyValue("color") === "red"
      • el.style.removeProperty("color") removes it
  • Task 5: Additional element methods (AC: #6)

    • 5.1 Implement element.cloneNode(deep) in call_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) to crates/dom/src/document.rs
      • Return new HostObject for cloned node
    • 5.2 Implement element.contains(other) in call_method():
      • Walk ancestors of other checking if any match element
      • Return boolean. element.contains(element) returns true (contains itself).
      • Add contains(ancestor_id, descendant_id) to dom crate if missing
    • 5.3 Implement element.matches(selector) in call_method():
      • Parse selector string using selectors crate
      • Check if element matches the selector
      • Return boolean
      • Reuse existing selector matching from document.query_selector() path
    • 5.4 Implement element.closest(selector) in call_method():
      • Walk ancestors (including self) checking matches(selector) on each
      • Return first matching ancestor as HostObject, or Null
    • 5.5 Implement element.outerHTML getter in get_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
  • 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_api
      • cargo test -p dom
      • cargo test -p rust_browser --test js_dom_tests
      • cargo test -p rust_browser --test js_tests
      • cargo test -p rust_browser --test js_events
      • cargo 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

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.

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.rs follow existing patterns
  • Run just ci after 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.rs in crates/web_api/src/dom_host/
  • web_api depends on dom, css, selectors (all Layer 1, horizontal deps OK)
  • No unsafe code 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}}

Debug Log References

Completion Notes List

File List