Skip to content

feat: M4-3.9 Custom Elements v1 — Web Components trilogy complete#29

Open
send wants to merge 20 commits intomainfrom
feat/m4-3.9-custom-elements
Open

feat: M4-3.9 Custom Elements v1 — Web Components trilogy complete#29
send wants to merge 20 commits intomainfrom
feat/m4-3.9-custom-elements

Conversation

@send
Copy link
Owner

@send send commented Mar 23, 2026

Summary

Custom Elements v1 (WHATWG HTML §4.13) — completes the Web Components trilogy (Shadow DOM + template/slot + Custom Elements).

New crate: elidex-custom-elements (crates/dom/)

  • customElements.define(name, constructor, options?) — name validation (hyphen required, reserved names excluded), constructor storage, pending upgrade queue
  • customElements.get(name) — constructor retrieval
  • customElements.whenDefined(name) — returns pending Promise, resolved when define() is called
  • customElements.upgrade(root) — subtree walk with depth-limited recursion
  • Constructor invocation — via JsObject::construct() with state machine (Undefined → Precustomized → Custom/Failed)
  • connectedCallback — fires for element AND all CE descendants on appendChild/insertBefore
  • disconnectedCallback — fires for element AND all CE descendants on removeChild
  • attributeChangedCallback — fires for observed attributes only (static observedAttributes getter)
  • adoptedCallback — passes old/new document arguments (awaits adoptNode for M4-3.10)
  • HTML parser integration — marks <my-element> tags as CustomElementState::Undefined, upgraded on define()
  • is attributecreateElement('div', {is: 'my-div'}) + HTML parser <div is="my-div"> recognition
  • Reaction queue — batched processing at 3 checkpoints (eval, dispatch_event, session.flush), MAX_CE_DRAIN_ITERATIONS=16

Review: 3 rounds (R1: constructor exception + innerHTML CE + whenDefined Promise + is parser + observedAttributes cap; R2: nested CE lifecycle + adoptedCallback args + comment fixes; R3: depth limits on recursive walks)

4 commits, 21 files changed, +1,899 lines.
Test count: 4,121 (was 4,099). All passing, clippy clean.

Test plan

  • mise run ci — all passing locally
  • 3 rounds of spec compliance review — all findings resolved
  • Constructor exception → Failed state test
  • Nested CE connected/disconnected test
  • innerHTML + CE upgrade test
  • is attribute customized built-in test

🤖 Generated with Claude Code

send and others added 4 commits March 24, 2026 00:52
New crate: crates/dom/elidex-custom-elements/

Types:
- CustomElementRegistry: define/get/is_defined/queue_for_upgrade/lookup_by_is
- CustomElementDefinition: name, constructor_id, observed_attributes, extends
- CustomElementReaction: Connected/Disconnected/Adopted/AttributeChanged/Upgrade
- CustomElementState: CEState enum (Undefined/Failed/Uncustomized/Precustomized/Custom)
- is_valid_custom_element_name: WHATWG HTML §4.13.2 validation
- DefineError: InvalidName/AlreadyDefined with Display/Error impls

7 tests: define/get, invalid name rejection, duplicate rejection,
pending upgrade queue, lookup_by_is, name validation (valid + invalid).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ifecycle callbacks, upgrade, is attribute)

Step 2: customElements JS global
- customElements.define(name, constructor, options?) with validation
- customElements.get(name), whenDefined(name), upgrade(root)
- HostBridge: CE registry, constructor store, reaction queue
- Reaction drain checkpoints: after eval(), dispatch_event(), session.flush()

Step 3: Custom Element creation + constructor
- createElement checks for CE name → registry lookup → Upgrade reaction
- Undefined elements queued for pending upgrade
- invoke_custom_element_constructor via JsObject::construct()
- CustomElementState ECS component (Undefined → Precustomized → Custom/Failed)

Step 4: Lifecycle callbacks — connected/disconnected
- appendChild/removeChild enqueue Connected/Disconnected reactions for CE entities
- drain_custom_element_reactions() invokes JS callbacks

Step 5: attributeChangedCallback
- setAttribute checks observedAttributes → enqueues AttributeChanged reaction
- Callback receives (name, oldValue, newValue, namespace)

Step 6: HTML parser + upgrade
- Parser marks <my-element> tags with CustomElementState::Undefined
- Pipeline drains CE reactions after script execution + flush

Step 7: is attribute + customized built-in
- createElement({is: 'my-div'}) parses options and looks up CE by is value

12 new tests (define/get, errors, constructor, connected/disconnected,
attributeChanged, parser marks, upgrade walk, is attribute).
4,118 total tests. mise run ci clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…adoptedCallback args, tests

R1 fixes:
- Constructor exception test (Failed state on throw)
- innerHTML + CE upgrade test
- observedAttributes cap at 1000
- whenDefined() returns pending Promise (resolved on define())
- HTML parser marks is= attribute elements

R2 fixes:
- Nested CE connected/disconnected: recursive subtree walk enqueues
  reactions for ALL descendant custom elements, not just direct child
- whenDefined comment: removed misleading "simplified" label
- adoptedCallback: passes old_document/new_document arguments
- Nested CE test: outer+inner append → both connected, remove → both disconnected

3 new tests (constructor exception, innerHTML CE, nested CE lifecycle).
4,121 total tests. mise run ci clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added MAX_ANCESTOR_DEPTH guard to:
- walk_and_enqueue_upgrades_inner (customElements.upgrade subtree walk)
- enqueue_ce_reactions_for_subtree_inner (connected/disconnected lifecycle)

Prevents stack overflow on pathological DOM trees (>10,000 depth).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 23, 2026 17:32
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Custom Elements v1 support (completing the Web Components trilogy) by introducing a new elidex-custom-elements crate, wiring a customElements global into the Boa runtime, and integrating lifecycle reaction processing into script + DOM mutation checkpoints.

Changes:

  • Introduces elidex-custom-elements crate (registry, reaction types, CE state tracking, name validation).
  • Adds customElements global (define/get/whenDefined/upgrade) and CE lifecycle reaction draining in elidex-js-boa.
  • Integrates CE reaction enqueue/drain around SessionCore::flush() in the shell pipeline and marks CE candidates during HTML parsing / document.createElement.

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
crates/shell/elidex-shell/src/pipeline.rs Drains CE reactions after session.flush() during script execution/finalization.
crates/shell/elidex-shell/Cargo.toml Adds elidex-custom-elements dependency.
crates/script/elidex-js-boa/src/runtime/tests.rs Adds runtime-level Custom Elements tests.
crates/script/elidex-js-boa/src/runtime/mod.rs Adds CE reaction draining + mutation-record scanning + callbacks invocation helpers.
crates/script/elidex-js-boa/src/globals/mod.rs Registers new customElements global.
crates/script/elidex-js-boa/src/globals/element/core.rs Enqueues CE reactions on DOM child ops and setAttribute.
crates/script/elidex-js-boa/src/globals/document.rs Extends document.createElement to support { is } and CE upgrade tracking.
crates/script/elidex-js-boa/src/globals/custom_elements.rs Implements the customElements global object.
crates/script/elidex-js-boa/src/bridge.rs Stores CE registry/constructors/reaction queue + whenDefined resolvers in the bridge.
crates/script/elidex-js-boa/Cargo.toml Adds elidex-custom-elements dependency.
crates/dom/elidex-html-parser/src/convert.rs Marks parsed custom elements / is customized built-ins with CustomElementState.
crates/dom/elidex-html-parser/Cargo.toml Adds elidex-custom-elements dependency.
crates/dom/elidex-custom-elements/src/validation.rs Implements CE name validation (+ tests).
crates/dom/elidex-custom-elements/src/tests.rs Adds registry tests (define/dup/pending upgrade/lookup_by_is).
crates/dom/elidex-custom-elements/src/state.rs Adds CE lifecycle state component/types.
crates/dom/elidex-custom-elements/src/registry.rs Implements CE definition registry + pending-upgrade queue + lookup-by-is.
crates/dom/elidex-custom-elements/src/reaction.rs Defines CE reaction enum.
crates/dom/elidex-custom-elements/src/lib.rs Exposes CE APIs from the crate.
crates/dom/elidex-custom-elements/Cargo.toml New crate manifest.
Cargo.toml Adds new crate to workspace + workspace dependency map.
Cargo.lock Records new crate dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

1. Extract flush_with_ce_reactions helper for consistent CE processing
   at all flush sites in pipeline.rs
2. Validate options.is with is_valid_custom_element_name
3. Customized built-in: check extends match via ce_lookup_by_is
4. setAttribute: normalize attribute name to lowercase for CE checks
5. removeAttribute: fire attributeChangedCallback with new_value: None
6. MutationRecord subtree walk: depth-limited walk_subtree_for_ce for
   all descendants in added/removed nodes
7. is_connected_to_document: use MAX_ANCESTOR_DEPTH constant
8. GC Trace: mark when_defined_resolvers JsFunctions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 12 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…tor validation, define order)

1. constructor.construct() entity binding: documented limitation (M4-9)
2. UnbindGuard in drain_custom_element_reactions (panic safety)
3. walk_subtree_for_ce: check is_connected_to_document before Connected
4. is_connected_to_document: verify root is NodeKind::Document (not DocumentFragment)
5. adoptedCallback args: documented entity bits limitation (M4-3.10)
6. enqueue_ce_reactions_for_subtree: connected tree check before Connected
7. customElements.define: validate constructor is callable
8. upgrade(): verify extends tag match for customized built-ins
9. register_custom_element: pre-validate name before allocating constructor ID
10-11. Documented pending queue tag-matching limitation
12. Verified no bare session.flush() in content/mod.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…n upgrade, xml prefix

1. is_callable → is_constructor for define() constructor validation
2. DOM walk after define() to upgrade parser-created Undefined elements
3. Extends tag verification in Upgrade handler (skip + Failed on mismatch)
4. ce_extends_tag() bridge helper (avoids nested RefCell borrows)
5. xml prefix rejection in is_valid_custom_element_name
6. Updated queue_for_ce_upgrade doc comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…ributes lowercase

1. innerHTML CE test: documented why full reaction pipeline isn't called
2. re_render bare flush: added CE reaction pipeline (enqueue + drain)
3. innerHTML-parsed Undefined CE: walk_subtree_for_upgrade enqueues
   Upgrade reactions for added nodes in Undefined state
4. observedAttributes: normalize to ASCII lowercase on extraction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…ropagation, doc comment

1. MutationRecord disconnected: only fire if parent was connected
2. removeChild disconnected: check parent is_connected_to_document
3. extract_observed_attributes: propagate getter/array errors (JsResult)
4. Doc comment: match actual call order (flush → CE → observers)
5. define() DOM walk: document O(n) performance note

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

… TypeError on non-callable callback

1. Upgrade handler: skip if element already Custom/Failed (dedup from pending + DOM walk)
2. whenDefined(): cache pending Promise per name, return same instance on repeat calls
3. invoke_ce_callback: non-callable callback property logs TypeError (was silent skip)
4. Test renamed: inner_html_triggers_ce_upgrade → inner_html_marks_custom_element_state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@send send requested a review from Copilot March 24, 2026 08:43
@send
Copy link
Owner Author

send commented Mar 25, 2026

@copilot

…ment

1. re_render flush loop: enqueue_ce_reactions_from_mutations receives
   only follow_up records (not accumulated), preventing duplicate reactions
2. extract_observed_attributes doc: clarify TypeError on non-array-like

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…on constant, test update

1. Duplicate customElements.define: TypeError (NotSupportedError) instead of SyntaxError
2. Stabilization loop: named constant MAX_CE_STABILIZATION_ROUNDS = 8
3. Test updated: expect TypeError for duplicate define
4. upgrade() comment: documents element-only limitation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hronous throw)

Per WHATWG HTML §4.13.4, customElements.whenDefined() must return a
Promise rejected with SyntaxError, not throw synchronously.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…dChild upgrade, is attribute in DOM

1. invoke_ce_callback: comment matches implementation (log, not throw)
2. removeAttribute: skip attributeChangedCallback if attribute didn't exist
3. MutationRecord: skip attributeChangedCallback if old == new value
4. appendChild/insertBefore: walk_subtree_for_upgrade for Undefined CEs
5. createElement({is}): set is attribute on element in DOM Attributes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

… attribute

1. attributeChangedCallback: fire whenever MutationRecord exists
   (spec doesn't gate on old != new value)
2. createElement({is}): call dom.rev_version() after setting is attribute
   to invalidate version-based caches

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…te non-allocating, attribute guard spec fix

1. Stabilization loop: log warning when MAX_CE_STABILIZATION_ROUNDS hit
2. ce_is_observed_attribute: non-allocating contains check (replaces Vec clone)
3. attributeChangedCallback: fire whenever MutationRecord exists (removed old==new guard)
4. createElement({is}): call rev_version after setting is attribute
5. Collapsed nested if statements (clippy)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…, NotSupportedError prefix, mixin CE doc

1. observedAttributes: sort + dedup to prevent duplicate callbacks
2. Consolidated walk_subtree_for_ce into enqueue_ce_reactions_for_subtree (single impl)
3. Duplicate define error: "NotSupportedError: " prefix for DOMException style
4. ChildNode/ParentNode mixin: documented CE limitation (direct DOM ops bypass reactions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…tured, construct/adopted comments

1. attributeChangedCallback: removed 4th namespace arg (spec is 3 args)
2. DefineError: structured enum propagation (no string matching)
   - InvalidName → SyntaxError, AlreadyDefined → TypeError (NotSupportedError)
   - DefineError exported from elidex-custom-elements
3. construct() return value: already documented (M4-9) — resolve
4. adoptedCallback f64 args: already documented (M4-3.10) — resolve

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

1. drain_custom_element_reactions: log warning at max iterations
2. CeReactionKind enum (Connected/Disconnected) replaces stringly-typed
   reaction_type parameter — compile-time type safety

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants