feat: extend lvt-el: to support native DOM event triggers#49
Conversation
Release @livetemplate/client v0.8.17 This release follows the core library version: 0.8.x 🤖 Generated with automated release script
…rigger
lvt-el:{method}:on:{event} now works with any native DOM event, not just
lifecycle states. This eliminates the need for inline JS onclick/onfocus
handlers, making templates CSP-safe and consistent with the declarative
lvt-* pattern.
Examples:
lvt-el:toggleClass:on:click="open" — toggle on click
lvt-el:addClass:on:focusin="open" — open on focus
lvt-el:removeClass:on:focusout="open" — close on blur
lvt-el:addClass:on:mouseenter="visible" — show on hover
lvt-el:removeClass:on:mouseleave="visible" — hide on leave
The trigger resolution:
- Lifecycle states (pending/success/error/done): existing behavior
- click-away: synthetic, handled by setupClickAwayDelegation
- Everything else: treated as native DOM event, delegated via
setupDOMEventTriggerDelegation
Non-bubbling events (mouseenter, mouseleave, focus, blur) use direct
element attachment. Bubbling events use wrapper-level delegation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewOverall this is clean and well-tested. Two issues worth addressing: Bug: Bubbling handler walks ancestor chain without bounds checkIn while (target && target !== wrapperElement.parentElement) {
processElementInteraction(target, trigger);
target = target.parentElement;
}This means clicking a nested element will fire handlers on all ancestors up to and including the wrapper. If both a If ancestor propagation is intentional, document it. If not, change the loop body to only call Correctness:
|
There was a problem hiding this comment.
Pull request overview
Extends the reactive lvt-el:{method}:on:{trigger} attribute system so non-lifecycle triggers can be native DOM events (e.g. click, focusin, mouseenter) handled via client-side event delegation (CSP-safe; avoids inline handlers).
Changes:
- Add native DOM event trigger support by skipping non-lifecycle triggers in
parseReactiveAttribute()and introducingprocessElementInteraction()+isDOMEventTrigger(). - Add
EventDelegator.setupDOMEventTriggerDelegation()and invoke it fromLiveTemplateClient.connect(). - Add unit tests for DOM trigger classification and element interaction processing; bump version + update changelog.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| VERSION | Bumps library version to 0.8.17. |
| package.json | Updates npm package version to 0.8.17. |
| package-lock.json | Updates lockfile version metadata to 0.8.17. |
| livetemplate-client.ts | Calls new DOM-trigger delegation setup during client connect. |
| dom/reactive-attributes.ts | Adds DOM-trigger helpers and makes the lifecycle parser ignore interaction triggers handled elsewhere. |
| dom/event-delegation.ts | Implements DOM event trigger delegation for lvt-el:*:on:{event} attributes. |
| tests/reactive-attributes.test.ts | Adds tests for DOM trigger classification and interaction processing. |
| CHANGELOG.md | Adds v0.8.17 entry (currently not aligned with PR content). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // Set up DOM event trigger delegation for lvt-el:*:on:{event} attributes | ||
| this.eventDelegator.setupDOMEventTriggerDelegation(); | ||
|
|
There was a problem hiding this comment.
setupDOMEventTriggerDelegation is only invoked during connect(), but the new DOM-event triggers need to work for elements added/changed by subsequent LiveTemplate updates (morphdom) and by handleNavigationResponse(). As-is, newly rendered elements will miss direct listeners (mouseenter/mouseleave/focus/blur) and newly introduced bubbling triggers won’t be delegated. Consider re-running setupDOMEventTriggerDelegation after each DOM update/navigation, or switching to a MutationObserver-based approach similar to setupAutofocusDelegation so new triggers/elements are handled automatically.
| // Re-run DOM event trigger delegation after later DOM updates | |
| // (for example morphdom patches and navigation-driven DOM changes), | |
| // so newly added elements/attributes are wired up as well. | |
| if (!document.documentElement.dataset.lvtDomEventTriggerObserverInstalled) { | |
| document.documentElement.dataset.lvtDomEventTriggerObserverInstalled = | |
| "true"; | |
| let domEventTriggerSetupScheduled = false; | |
| const scheduleDOMEventTriggerDelegationSetup = () => { | |
| if (domEventTriggerSetupScheduled) { | |
| return; | |
| } | |
| domEventTriggerSetupScheduled = true; | |
| queueMicrotask(() => { | |
| domEventTriggerSetupScheduled = false; | |
| this.eventDelegator.setupDOMEventTriggerDelegation(); | |
| }); | |
| }; | |
| new MutationObserver((mutations) => { | |
| for (const mutation of mutations) { | |
| if (mutation.type === "childList") { | |
| scheduleDOMEventTriggerDelegationSetup(); | |
| return; | |
| } | |
| if ( | |
| mutation.type === "attributes" && | |
| mutation.attributeName && | |
| mutation.attributeName.includes(":on:") | |
| ) { | |
| scheduleDOMEventTriggerDelegationSetup(); | |
| return; | |
| } | |
| } | |
| }).observe(document.documentElement, { | |
| subtree: true, | |
| childList: true, | |
| attributes: true, | |
| }); | |
| } |
dom/event-delegation.ts
Outdated
| import { debounce, throttle } from "../utils/rate-limit"; | ||
| import { lvtSelector } from "../utils/lvt-selector"; | ||
| import { executeAction, type ReactiveAction } from "./reactive-attributes"; | ||
| import { executeAction, processElementInteraction, isDOMEventTrigger, SYNTHETIC_TRIGGERS, type ReactiveAction } from "./reactive-attributes"; |
There was a problem hiding this comment.
The new import list includes SYNTHETIC_TRIGGERS but it isn’t used anywhere in this file. Please remove the unused import to avoid unnecessary bundle/code noise and to keep the import list accurate.
| import { executeAction, processElementInteraction, isDOMEventTrigger, SYNTHETIC_TRIGGERS, type ReactiveAction } from "./reactive-attributes"; | |
| import { executeAction, processElementInteraction, isDOMEventTrigger, type ReactiveAction } from "./reactive-attributes"; |
dom/event-delegation.ts
Outdated
| let target = e.target as Element | null; | ||
| while (target && target !== wrapperElement.parentElement) { | ||
| processElementInteraction(target, trigger); |
There was a problem hiding this comment.
In the delegated bubbling-event handler, the ancestor walk terminates at wrapperElement.parentElement. This makes the traversal boundary dependent on the wrapper’s parent and can accidentally walk beyond the wrapper if parentElement is null (or if the wrapper is ever moved). It’s safer and clearer to stop at the wrapper element itself (and optionally process the wrapper separately) and/or guard with wrapperElement.contains(target) before walking.
| let target = e.target as Element | null; | |
| while (target && target !== wrapperElement.parentElement) { | |
| processElementInteraction(target, trigger); | |
| const targetNode = e.target as Node | null; | |
| if (!(targetNode instanceof Element) || !wrapperElement.contains(targetNode)) return; | |
| let target: Element | null = targetNode; | |
| while (target) { | |
| processElementInteraction(target, trigger); | |
| if (target === wrapperElement) break; |
dom/event-delegation.ts
Outdated
| * Scans the wrapper for elements with these attributes, attaches direct | ||
| * listeners for non-bubbling events (mouseenter, mouseleave) and delegated | ||
| * listeners on the wrapper for bubbling events (click, focusin, focusout, etc.). | ||
| * | ||
| * Called after each render/update to handle new elements. |
There was a problem hiding this comment.
The docstring says setupDOMEventTriggerDelegation is “Called after each render/update to handle new elements”, but the only call site appears to be during connect(). Either update the comment to reflect actual behavior, or ensure the method is actually invoked after updates/navigation so the documentation stays correct.
| * Scans the wrapper for elements with these attributes, attaches direct | |
| * listeners for non-bubbling events (mouseenter, mouseleave) and delegated | |
| * listeners on the wrapper for bubbling events (click, focusin, focusout, etc.). | |
| * | |
| * Called after each render/update to handle new elements. | |
| * Scans the current wrapper for elements with these attributes, attaches | |
| * direct listeners for non-bubbling events (mouseenter, mouseleave) and | |
| * ensures delegated listeners exist on the wrapper for bubbling events | |
| * (click, focusin, focusout, etc.). |
| /** | ||
| * Sets up event listeners for lvt-el:*:on:{event} attributes where {event} | ||
| * is a native DOM event (not a lifecycle state or synthetic trigger). | ||
| * | ||
| * Scans the wrapper for elements with these attributes, attaches direct | ||
| * listeners for non-bubbling events (mouseenter, mouseleave) and delegated | ||
| * listeners on the wrapper for bubbling events (click, focusin, focusout, etc.). | ||
| * | ||
| * Called after each render/update to handle new elements. | ||
| */ | ||
| setupDOMEventTriggerDelegation(): void { | ||
| const wrapperElement = this.context.getWrapperElement(); | ||
| if (!wrapperElement) return; | ||
|
|
||
| const wrapperId = wrapperElement.getAttribute("data-lvt-id"); | ||
| // Non-bubbling events need direct attachment | ||
| const NON_BUBBLING = new Set(["mouseenter", "mouseleave", "focus", "blur"]); | ||
| // Track which bubbling events we've already delegated at wrapper level | ||
| const delegatedKey = `__lvt_el_delegated_${wrapperId}`; | ||
| const delegated: Set<string> = (wrapperElement as any)[delegatedKey] || new Set(); | ||
|
|
||
| // Scan all elements for lvt-el:*:on:{event} attributes | ||
| wrapperElement.querySelectorAll("*").forEach(el => { | ||
| const triggers = new Set<string>(); | ||
| for (const attr of el.attributes) { | ||
| if (!attr.name.startsWith("lvt-el:")) continue; | ||
| const match = attr.name.match(/^lvt-el:\w+:on:([a-z-]+)$/i); | ||
| if (!match) continue; | ||
| const trigger = match[1].toLowerCase(); | ||
| if (!isDOMEventTrigger(trigger)) continue; | ||
| triggers.add(trigger); | ||
| } | ||
|
|
||
| for (const trigger of triggers) { | ||
| if (NON_BUBBLING.has(trigger)) { | ||
| // Direct attachment for non-bubbling events | ||
| const key = `__lvt_el_${trigger}`; | ||
| if ((el as any)[key]) continue; // already attached | ||
| const listener = () => processElementInteraction(el, trigger); | ||
| el.addEventListener(trigger, listener); | ||
| (el as any)[key] = listener; | ||
| } else if (!delegated.has(trigger)) { | ||
| // Delegated listener on wrapper for bubbling events | ||
| wrapperElement.addEventListener(trigger, (e: Event) => { | ||
| let target = e.target as Element | null; | ||
| while (target && target !== wrapperElement.parentElement) { | ||
| processElementInteraction(target, trigger); | ||
| target = target.parentElement; | ||
| } | ||
| }); | ||
| delegated.add(trigger); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| (wrapperElement as any)[delegatedKey] = delegated; | ||
| } |
There was a problem hiding this comment.
No automated tests cover setupDOMEventTriggerDelegation (bubbling vs non-bubbling events, delegation boundary, and ensuring newly added elements/triggers work after updates). Since dom/event-delegation.ts already has extensive test coverage, adding focused tests for this new delegation path would help prevent regressions.
| ## [v0.8.17] - 2026-04-05 | ||
|
|
||
| ### Changes | ||
|
|
||
| - fix: form.name DOM shadowing + skip File objects in FormData parsing (58cf0c2) | ||
|
|
There was a problem hiding this comment.
The v0.8.17 changelog entry doesn’t mention the new “native DOM event triggers for lvt-el:*:on:{event}” feature described in the PR, and instead references an unrelated FormData/form.name fix. Please update the entry so the changelog accurately reflects the changes being released in 0.8.17 (or move the unrelated note to the appropriate release).
| * Reactive Attributes - Declarative DOM actions triggered by lifecycle events or interactions. | ||
| * | ||
| * Attribute Pattern: lvt-el:{method}:on:[{action}:]{state|interaction}="param" | ||
| * | ||
| * States (lifecycle): | ||
| * States (lifecycle — server action request-response cycle): | ||
| * - pending: Action started, waiting for server response | ||
| * - success: Action completed successfully | ||
| * - error: Action completed with validation errors | ||
| * - done: Action completed (regardless of success/error) | ||
| * | ||
| * Interactions: | ||
| * - click-away: Click outside the element (handled by setupClickAwayDelegation) | ||
| * Interactions (client-side — no server round-trip): | ||
| * - Any native DOM event (click, focusin, focusout, mouseenter, mouseleave, keydown, etc.) | ||
| * - click-away: Synthetic — click outside the element (not a native DOM event) | ||
| * |
There was a problem hiding this comment.
The header docs/pattern imply that the optional {action}: scope applies to {state|interaction}, but in practice action scoping is only supported for lifecycle states. Interaction triggers (click-away and native DOM events) are always handled outside parseReactiveAttribute and don’t support action:trigger forms (e.g. on:save:click). Consider updating the docs to avoid implying scoped interactions are supported.
…iggers
lvt-fx:{effect}:on:{trigger} now supports three trigger types:
- Implicit (no :on:): fires on every DOM update (existing behavior)
- Lifecycle (:on:success, :on:pending, etc.): fires on action lifecycle
- DOM event (:on:click, :on:mouseenter, etc.): fires on native DOM event
Examples:
lvt-fx:highlight:on:click="flash" — highlight on click
lvt-fx:highlight:on:mouseenter="flash" — highlight on hover
lvt-fx:highlight:on:success="flash" — highlight on action success
lvt-fx:animate:on:click="fade" — animate on click
Refactored directive handlers to share a common applyFxEffect() function
used by all three trigger paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewGood feature overall — clean separation of lifecycle vs. DOM event triggers, solid test coverage. Three issues worth addressing: 1. Bubbling delegation fires on ALL ancestors (bug)In while (target && target !== wrapperElement.parentElement) {
processElementInteraction(target, trigger);
target = target.parentElement;
}This walks all the way up the DOM calling <div lvt-el:addClass:on:click="outer">
<button lvt-el:addClass:on:click="inner"><span>Click</span></button>
</div>Clicking 2.
|
- Fix bubbling delegation to stop at closest matching element instead of walking all ancestors (was triggering both parent and child) - Add deduplication guard to setupFxLifecycleListeners (prevents listener stacking on reconnect) - Use processFxLifecycleAttributes in lifecycle listener (remove duplication) - Remove unused SYNTHETIC_TRIGGERS import from event-delegation.ts - Call setupDOMEventTriggerDelegation after each DOM update (not just connect) - Clarify doc header: action scoping only applies to lifecycle triggers - Add Unreleased section to CHANGELOG with new features Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewGood feature overall — the separation of lifecycle vs. DOM event vs. synthetic triggers is clean, and the dedup guards ( Bug: redundant attribute guard checks (dead code)In if (!htmlElement.hasAttribute("lvt-fx:scroll")) return;...are always Memory leak: lifecycle listeners never removed
Consider storing the listeners on the instance and removing them in Duplicate logic in
|
…FxEffect Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewOverall the design is clean and the test coverage is solid. A few issues worth addressing: Bug: Stale closure in
|
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewGood feature overall — clean separation of lifecycle vs DOM event triggers, and the delegation approach for bubbling events is the right call. Bug: overlapping highlight effects In const originalBackground = htmlElement.style.backgroundColor;
htmlElement.style.backgroundColor = color;
setTimeout(() => {
htmlElement.style.backgroundColor = originalBackground; // may capture highlighted color
...
}, 50);If the effect fires twice in quick succession (e.g., fast repeated clicks), the second invocation captures the highlighted color as Performance: RegExp created inside event handler In wrapperElement.addEventListener(trigger, (e: Event) => {
...
a.name.match(new RegExp(`^lvt-el:\\w+:on:${trigger}$`, "i")) // created per-eventSince Stale listeners on attribute removal
Minor: CHANGELOG inconsistency The new DOM-event-trigger features are listed under |
- Skip highlight if already mid-animation to prevent stale capture - Hoist RegExp out of event listener closure for performance - Guard against morphdom-removed attributes in fx listeners Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review: feat: extend lvt-el: to support native DOM event triggersOverall the approach is clean — good separation of DOM event triggers from lifecycle and synthetic triggers, delegated listeners for bubbling events, and the Issues worth addressing1. Leaked listeners on removed elements (directives.ts, event-delegation.ts) In Same issue in Consider cleaning up via morphdom's 2. Module-level let fxLifecycleListenersSetup = false;This won't reset between test runs — 3. Bubbling delegation stops at first match (event-delegation.ts ~line 486) if (hasMatch) {
processElementInteraction(target, trigger);
return; // Stop at closest match
}If a parent and child both have Minor
|
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewGood addition overall — the DOM event trigger approach is clean and the test coverage is solid. A few issues worth addressing: Bug:
|
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewOverall solid refactor — the unified Bug:
let fxLifecycleListenersSetup = false;
export function setupFxLifecycleListeners(): void {
if (fxLifecycleListenersSetup) return;
...
document.addEventListener(`lvt:${lifecycle}`, (e) => {
processFxLifecycleAttributes(document.documentElement, lifecycle, actionName);
});
}Two problems:
Scoping the scan to each instance's wrapper element (and accepting the wrapper as a parameter rather than using a module-level guard) would fix both. Tests removed from CI without explanation
Minor: null key collision in const wrapperId = wrapperElement.getAttribute("data-lvt-id");
const delegatedKey = `__lvt_el_delegated_${wrapperId}`;If |
- setupFxLifecycleListeners now accepts rootElement param and guards per-wrapper instead of module singleton — fixes multi-instance pages - Add null guard for wrapperId in setupDOMEventTriggerDelegation - Add comment explaining removed examples from cross-repo CI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewGood addition of DOM-event and lifecycle triggers for Bug: Listener leak on disconnect
Recommend storing the lifecycle listener references and removing them in the client's disconnect/destroy path. Performance: Double full-DOM scan on every updateAfter each DOM update, both Minor: Delegated handler silently drops parent matchesIn Nothing blocking from a security standpoint. The attribute-name allowlist ( |
…btree - setupFxLifecycleListeners stores listener refs; new teardownFxLifecycleListeners removes them on disconnect to prevent accumulation across reconnects - setupDOMEventTriggerDelegation accepts optional scanRoot param; after DOM updates only the patched subtree is scanned instead of the full wrapper - Document closest-match-only bubbling semantics in JSDoc Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review: DOM Event Triggers for lvt-el/lvt-fxGood overall design — the trigger routing is clean and the guard against duplicate listeners is well thought out. A few issues worth addressing: Bug: Delegated listeners leak on disconnect
If the same Concern: Unrestricted event name acceptance
Minor: CHANGELOG double blank lineExtra blank line after the v0.8.17 entry — trivial cleanup. Looks good
|
- Add teardownDOMEventTriggerDelegation to remove wrapper-level listeners on disconnect, preventing stale handlers across reconnects - Document open event name acceptance in isDOMEventTrigger JSDoc - Clean up CHANGELOG double blank line Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Good overall structure and the lifecycle/DOM-event separation is clean. A few issues worth addressing: Bug: Non-bubbling listeners not cleaned up on teardown teardownDOMEventTriggerDelegation removes wrapper-level delegated listeners, but direct mouseenter/mouseleave/focus/blur listeners attached to child elements (stored as (el as any)[_lvt_el${trigger}]) are never removed. If the component disconnects and DOM elements are reused across reconnects (morphdom keeping existing nodes), stale listeners accumulate. Consider storing per-element direct listener references on the wrapper alongside the delegated ones so teardown is complete. Behavioral change in highlight guard applyFxEffect adds a __lvtHighlighting guard that silently skips re-triggering during an in-flight highlight. The previous handleHighlightDirectives had no such guard -- each DOM update would restart the animation. This is arguably better, but it changes behavior for all existing lvt-fx:highlight users (not just the new 🔛 path) and is not called out in the changelog. Also: __lvtHighlighting is never cleared if the element is removed mid-animation; the inner setTimeout reset should guard with if (el.isConnected) for clarity. Minor: delegation stops at closest match even for unrecognized methods In the bubbling handler, hasMatch returns true for any lvt-el:*:on:{trigger} attribute including ones with methods not in METHOD_MAP. The early return still swallows the event for ancestor elements, so a method typo silently breaks parent handlers too. Low risk with the current closed method set, but worth noting. |
…ight - Track both direct (non-bubbling) and delegated listeners in unified array on wrapper for complete cleanup in teardownDOMEventTriggerDelegation - Guard highlight timeout with isConnected check to prevent style writes on removed elements and ensure __lvtHighlighting flag is always cleared Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewGood overall design — clean separation between lifecycle, synthetic, and DOM event triggers. The delegation approach (bubbling on wrapper, direct for non-bubbling) and the teardown symmetry look solid. A few things worth looking at: Missing teardown for
The switch statement in Highlight timeout restores stale background
Minor: In |
…eners - Add teardownFxDOMEventTriggers to remove direct lvt-fx listeners on disconnect, wired up alongside existing lifecycle/delegation teardowns - Add default case to applyFxEffect warning on unknown effect names - Hoist allListeners read/write outside forEach loop in delegation setup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewOverall the approach is clean and well-structured. A few issues worth addressing: Memory leak — stale element references in listener arraysBoth
When morphdom replaces an element, the new element gets a fresh listener attached (correct), but the old element stays in the array holding a reference indefinitely. On pages with frequent server pushes, this is a memory leak. The teardown on disconnect clears everything, but only at disconnect time. Fix: Before pushing to the array, filter out already-disconnected elements: // Prune stale entries before appending
const pruned = fxListeners.filter(entry => entry.el.isConnected);
pruned.push({ el, event: parsed.trigger, handler: listener });
(rootElement as any)[fxListenersKey] = pruned;Same pattern applies in
|
Filter out disconnected elements before appending new entries to prevent unbounded growth in __lvtFxDirectListeners and __lvt_el_listeners arrays when morphdom replaces DOM elements across frequent server pushes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewGood overall structure — the refactor into Bug:
|
- teardownFxDOMEventTriggers now deletes per-element __lvt_fx_* markers so surviving elements re-attach listeners correctly on reconnect - setupDOMEventTriggerDelegation and setupFxDOMEventTriggers now include the root element in the scan (querySelectorAll only returns descendants) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewGood feature overall — clean architecture with proper setup/teardown pairs. A few things to check: Bug:
[rootElement, ...rootElement.querySelectorAll("*")].forEach(el => { ... });New The old Minor: delegated handler creates a new array per event per traversed element In const hasMatch = Array.from(target.attributes).some(a => triggerPattern.test(a.name));For high-frequency events (mousemove, scroll) this allocates on every element traversal. A No issues with the event delegation logic, teardown pairs, or the capture listener scoping for lifecycle events. |
- processFxLifecycleAttributes now includes rootElement itself in scan - Replace Array.from().some() with for..of loop in delegation handler to avoid per-event allocation on high-frequency triggers - Document intentional highlight rate-limiting behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Review: feat: extend lvt-el: to support native DOM event triggers Overall the design is solid — clean delegation model, good teardown discipline, well-tested. A few issues worth addressing: Bug: The outer Fix: reset the flag in the early-return path, and add an setTimeout(() => {
if (!htmlElement.isConnected) {
(htmlElement as any).__lvtHighlighting = false; // ← was missing
return;
}
htmlElement.style.backgroundColor = originalBackground;
setTimeout(() => {
if (htmlElement.isConnected) htmlElement.style.transition = originalTransition;
(htmlElement as any).__lvtHighlighting = false;
}, duration);
}, 50);Performance: spreading NodeList on every DOM update ( [rootElement, ...rootElement.querySelectorAll("*")].forEach(el => { ... })This runs after every morphdom patch. For large DOMs, spreading a NodeList into a heap-allocated array on each update creates GC pressure. Prefer iterating the NodeList directly and handling
const NON_BUBBLING = new Set(["mouseenter", "mouseleave", "focus", "blur"]);This is inside Silent typo footgun with open-ended event name acceptance The comment in |
…timeout - Replace [root, ...querySelectorAll] with processEl(root) + forEach to avoid heap-allocating an array on every DOM update - Lift NON_BUBBLING set to module-level const - Add isConnected guard in inner highlight setTimeout Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review: DOM Event Triggers for lvt-el/lvt-fxOverall a well-structured addition. Setup/teardown symmetry is correct, the delegated-vs-direct split for non-bubbling events is sound, and per-element guard keys prevent duplicate listener attachment. Bug: stale inline styles on disconnect during highlightIn setTimeout(() => {
if (!htmlElement.isConnected) {
(htmlElement as any).__lvtHighlighting = false;
return; // backgroundColor and transition never restored
}
...
}, 50);If morphdom later reuses the same element node, it will inherit the stale highlight color and transition, causing visual artifacts. Fix: reset both styles unconditionally before the early return: if (!htmlElement.isConnected) {
htmlElement.style.backgroundColor = originalBackground;
htmlElement.style.transition = originalTransition;
(htmlElement as any).__lvtHighlighting = false;
return;
}Minor: open-ended event name acceptance
Looks good
|
Reset backgroundColor and transition to original values when element disconnects mid-highlight to prevent stale styles on morphdom reuse. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Review: DOM event trigger delegation One real bug and a few observations: Bug: guard keys not cleared on teardown for non-bubbling events
The Minor: no tests for the Tests were added for Design note: open event allowlist The comment on Rest looks good — the morphdom stale-entry pruning strategy is sound, lifecycle listener cleanup is properly scoped to the root element, and the capture-phase registration for |
Store guardKey in listener entries for non-bubbling events so teardownDOMEventTriggerDelegation can delete per-element markers, allowing re-attachment on reconnect with reused DOM elements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewOverall: Clean feature addition with good teardown discipline. One real bug worth fixing, one minor inconsistency. Bug:
|
setupFxDOMEventTriggers now accepts a separate registryRoot param so listener entries always live on the wrapper element regardless of which subtree is scanned. Ensures teardownFxDOMEventTriggers(wrapper) finds all entries including those registered during DOM-patch scans. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Good overall — the refactor is clean and teardown coverage is thorough. A few things worth addressing: 1. Unescaped user-derived RegExp in delegated handler (event-delegation.ts) const triggerPattern = new RegExp(`^lvt-el:\\w+:on:${trigger}$`, "i");
const escapedTrigger = trigger.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const triggerPattern = new RegExp(`^lvt-el:\\w+:on:${escapedTrigger}$`, "i");2. The old 3. Open-ended trigger classification ( The function is intentionally allowlist-free (documented), so typos like 4. Minor: redundant wrapper check in delegated handler if (target === wrapperElement) {
processElementInteraction(wrapperElement, trigger);
}This fires on every event that bubbles fully to the wrapper even when the wrapper has no matching attributes. Issues 1 and 2 are worth fixing before merge; 3 and 4 are optional. |
Prevent regex metacharacters in custom event names from producing incorrect match patterns in the bubbling delegation handler. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReviewOverall: Clean, well-structured PR. The delegation model (direct for non-bubbling, wrapper-delegated for bubbling) is the right approach. Teardown is properly implemented. A few notes: Potential Bug — stale listener memory in
|
Summary
lvt-el:{method}:on:{trigger}to accept any native DOM event as a trigger, not just lifecycle statesonclick,onfocus, etc.) in component templatesunsafe-inlineneededExamples
Trigger resolution
pending/success/error/done): existing behavior, unchangedclick-away: synthetic trigger, handled bysetupClickAwayDelegation, unchangedTest plan
processElementInteractionhandles addClass, removeClass, toggleClassisDOMEventTriggercorrectly classifies lifecycle vs DOM events🤖 Generated with Claude Code