Skip to content

feat(action,track): add a reserved mounted pseudo-event and generic $event paths - #658

Open
titouanmathis wants to merge 4 commits into
feat/v2-fetch-lifecycle-detailfrom
feat/v2-declarative-event-paths
Open

titouanmathis wants to merge 4 commits into
feat/v2-fetch-lifecycle-detailfrom
feat/v2-declarative-event-paths

Conversation

@titouanmathis

Copy link
Copy Markdown
Contributor

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 mounted pseudo-event for Action

data-on:mounted runs 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:

<form data-component="Fetch Action" data-on:mounted="Fetch(#content-search) -> target.fetch()"></form>

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 the on/target/effect option triple.

The effect receives undefined for its event argument. debounce applies; prevent, stop, once, capture and passive have nothing to act on and are documented as ignored.

Where the shared scheduling lives

packages/ui/src/utils/mounted-event.ts, next to event-modifiers.ts. It holds the reserved name and whenMounted(component, callback), which posts the work to defaultScheduler.background, guards on $isMounted, and returns its cancel. AbstractTrack.__bind() and the new Action.__attach() both use it.

Track's __deferred set 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 existing Track specs cover the cancel-on-unmount and remount paths.

#652 — generic $event paths, and fresh DOM-backed data

$event.<path> resolves against the whole event, so a native event works too:

<button data-track:click='{"event": "cta_click", "type": "$event.target.dataset.type"}'></button>

Resolution no longer requires a CustomEvent. The .detail modifier, which merges the detail wholesale, stays CustomEvent-only, because a native event has none.

$detail is kept as sugar, as the maintainer asked. It is a path rewrite, not a second code path: $detail.x becomes the path detail.x walked 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 yields undefined, 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, returns undefined for a missing path and for every placeholder when there is no event at all, and knows nothing about Fetch or any other emitter.

resolveDetailPlaceholders is renamed resolveEventPlaceholders, updated in packages/ui/src/Track/index.ts. It is not in the package root barrel, so public-contracts.ts and the barrel snapshot are unaffected.

Fresh DOM-backed data

AbstractTrack no longer caches __payload and __context. The inherited TrackContext, the payload script and the payload option are read when the event fires, so a Fetch that 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 a mounted and a js-toolkit:component:mounted event bubbling from a descendant; cancels on unmount; one new effect per remount; undefined event; debounce; the on option.

packages/tests/Track/TrackEvent.spec.ts$event.* on a CustomEvent, 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 morphed TrackContext script, 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 real Fetch update read through data-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:check and npm run docs:build all pass.

Closes #648
Closes #652

🤖 Generated with Claude Code

https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu

titouanmathis and others added 3 commits September 17, 2026 01:10
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

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.32%. Comparing base (ae61c0e) to head (368f135).

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           
Flag Coverage Δ
unittests 86.32% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

Code Review

Risk: Low — no blocking issues; safe to merge aside from nits.

The change centralizes deferred mount scheduling for Action and Track, preserves $detail as shorthand for $event.detail, and resolves placeholders against native and custom events. It also removes mount-cycle caching for Track context and payload sources and adds documentation and coverage for the new behavior.


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 runs

Previous run archived 2026-09-17T10:16:17Z

Code Review

Risk: Low — The changes add the reserved mounted Action event, generic event-path resolution, and per-dispatch Track payload reads without introducing a demonstrable defect in the reviewed diff.

The implementation centralizes deferred mount scheduling for Action and Track, resolves $event and $detail placeholders through nested objects and arrays, and removes mount-cycle caching for DOM-backed tracking data. Documentation and tests cover the new behavior, including cancellation, remounting, native events, lifecycle events, and morphed payload sources.


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
@titouanmathis
titouanmathis added this pull request to stack #660 September 17, 2026 10:48
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.

1 participant