feat: M4-3.9 Custom Elements v1 — Web Components trilogy complete#29
feat: M4-3.9 Custom Elements v1 — Web Components trilogy complete#29
Conversation
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>
There was a problem hiding this comment.
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-elementscrate (registry, reaction types, CE state tracking, name validation). - Adds
customElementsglobal (define/get/whenDefined/upgrade) and CE lifecycle reaction draining inelidex-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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
…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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
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/)<my-element>tags as CustomElementState::Undefined, upgraded on define()isattribute —createElement('div', {is: 'my-div'})+ HTML parser<div is="my-div">recognitionReview: 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🤖 Generated with Claude Code