feat(action,track): add a reserved mounted pseudo-event and generic $event paths - #658
titouanmathis wants to merge 4 commits into
Conversation
Let HTML run an Action once its element and the components sharing it are mounted, without binding the public component lifecycle DOM event. `Track` already deferred its own `mounted` declaration that way. Lift the scheduling into `utils/mounted-event.ts` and have both families use it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu
…atch Placeholders now resolve against the whole event, so `$event.target.dataset.type` works on a native event and `$event.detail.response.headers.x-count` on a component event. `$detail.x` is rewritten to the path `detail.x` walked from the same root. The inherited context, the payload script and the payload option are read when the event fires, so a partial DOM update changes what the next dispatch sends. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## feat/v2-fetch-lifecycle-detail #658 +/- ##
=================================================================
Coverage 86.32% 86.32%
Complexity 145 145
=================================================================
Files 20 20
Lines 746 746
Branches 88 88
=================================================================
Hits 644 644
Misses 95 95
Partials 7 7
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
Code ReviewRisk: Low — no blocking issues; safe to merge aside from nits. The change centralizes deferred mount scheduling for Action and Track, preserves Review usage: 47,030 in (3,677 cached) / 913 out tokens — $0.0300 (openrouter/openai/gpt-5.6-luna, thinking: low) Reviewed by @weareikko/code-review v0.9.5 for commit 368f135. Previous review runsPrevious run archived 2026-09-17T10:16:17ZCode ReviewRisk: Low — The changes add the reserved The implementation centralizes deferred mount scheduling for Action and Track, resolves Review usage: 24,420 in (3,677 cached) / 636 out tokens — $0.0151 (openrouter/openai/gpt-5.6-luna, thinking: low) Reviewed by @weareikko/code-review v0.9.5 for commit f24d695. |
The debounce spec asserted a moment at which the effect had not run yet, which a machine slower than the delay reaches too late. Assert the gap between an undebounced and a debounced declaration instead: a timer cannot fire early, so the gap is the delay however loaded the machine is. Poll for every effect that has run, since the deferred work lands in the background lane a single settle() is generous about rather than deterministic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu
Stacks on #657, which stacks on #656. Review the last two commits only until those land; GitHub retargets this PR as the stack merges.
#648 — a reserved
mountedpseudo-event forActiondata-on:mountedruns the effect once per mount cycle, after the current mount batch has settled, so the effect can target a component mounted on the same element:It binds no DOM listener, so a lifecycle event bubbling from a descendant that mounts later never runs it again. Unmounting before the deferred effect runs cancels it, and remounting starts exactly one new one. Both halves of the component go through it: the
data-on:*namespace and theon/target/effectoption triple.The effect receives
undefinedfor itseventargument.debounceapplies;prevent,stop,once,captureandpassivehave nothing to act on and are documented as ignored.Where the shared scheduling lives
packages/ui/src/utils/mounted-event.ts, next toevent-modifiers.ts. It holds the reserved name andwhenMounted(component, callback), which posts the work todefaultScheduler.background, guards on$isMounted, and returns its cancel.AbstractTrack.__bind()and the newAction.__attach()both use it.Track's__deferredset is gone rather than moved:watchAttributeNamespace's cleanup releases every binding, and each binding's release already cancels its own task, so the set cancelled nothing the release had not. Behaviour is unchanged — the existingTrackspecs cover the cancel-on-unmount and remount paths.#652 — generic
$eventpaths, and fresh DOM-backed data$event.<path>resolves against the whole event, so a native event works too:Resolution no longer requires a
CustomEvent. The.detailmodifier, which merges the detail wholesale, staysCustomEvent-only, because a native event has none.$detailis kept as sugar, as the maintainer asked. It is a path rewrite, not a second code path:$detail.xbecomes the pathdetail.xwalked from the event, so the two roots can never disagree. That rewrite also removed the special case for a non-object detail — descending into a primitive already yieldsundefined, which is the same answer the special case gave. The change is therefore non-breaking, so nothing goes in the migration guide.The resolver walks objects and arrays, reads a numeric segment as an array index (
request.searchParams.genre.0), recurses through nested objects and arrays in the declared payload, returnsundefinedfor a missing path and for every placeholder when there is no event at all, and knows nothing aboutFetchor any other emitter.resolveDetailPlaceholdersis renamedresolveEventPlaceholders, updated inpackages/ui/src/Track/index.ts. It is not in the package root barrel, sopublic-contracts.tsand the barrel snapshot are unaffected.Fresh DOM-backed data
AbstractTrackno longer caches__payloadand__context. The inheritedTrackContext, the payload script and the payload option are read when the event fires, so aFetchthat morphs a JSON script or a context attribute changes what the next dispatch sends, with no remount. Merge precedence is unchanged.Tests
packages/tests/Action/Action.spec.ts— runs once after mount; reaches a component on its own element; binds no listener; ignores amountedand ajs-toolkit:component:mountedevent bubbling from a descendant; cancels on unmount; one new effect per remount;undefinedevent;debounce; theonoption.packages/tests/Track/TrackEvent.spec.ts—$event.*on aCustomEvent, a native event and a component event;$detail.*equal to$event.detail.*; numeric segments; missing path; no event; nested objects and arrays.packages/tests/Track/Track.spec.ts— a morphedTrackContextscript, a morphed payload script and a rewritten payload option each affect the next dispatch on the same instance; precedence unchanged when a source changes.packages/tests/Track/TrackFetch.spec.ts— a realFetchupdate read throughdata-track:fetch-update-after, resolving a response header, the request method, a repeated search param by index, and a missing header.npm run test(1037 passed),npm run lint,npm run manifest:checkandnpm run docs:buildall pass.Closes #648
Closes #652
🤖 Generated with Claude Code
https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu