Announce the data-bind:if change with the wrappable dom-update protocol event - #634
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Code ReviewRisk: Low — The change adds the bubbling Still open from earlier reviews (1 finding):
Review usage: 30,418 in (18,397 cached) / 1,143 out tokens — $0.0119 (openrouter/openai/gpt-5.6-luna, thinking: low) Reviewed by @weareikko/code-review v0.9.5 for commit ac6c541. Previous review runsPrevious run archived 2026-08-13T12:11:14ZCode ReviewRisk: Low — The change adds the shared bubbling The implementation tracks logical presence separately from rendered nodes, supports function and duck-typed transitioner runners, and preserves the DOM change when a runner rejects. Documentation, public type exports, changelog entries, and protocol-focused tests are included. Still open from earlier reviews (1 finding):
Review usage: 14,616 in (3,677 cached) / 512 out tokens — $0.0086 (openrouter/openai/gpt-5.6-luna, thinking: low) Reviewed by @weareikko/code-review v0.9.5 for commit 63c5958. Previous run archived 2026-08-13T12:05:18ZCode ReviewRisk: Medium — issues that should be addressed before merge. Adds a bubbling 1 issue found:
Still open from earlier reviews (1 finding):
Review usage: 60,760 in (46,006 cached) / 1,292 out tokens — $0.0154 (openrouter/openai/gpt-5.6-luna, thinking: low) Reviewed by @weareikko/code-review v0.9.5 for commit e91aa86. Previous run archived 2026-08-13T08:17:52ZCode ReviewRisk: Medium — issues that should be addressed before merge. Adds a bubbling 1 issue found:
Review usage: 37,522 in (24,090 cached) / 1,111 out tokens — $0.0129 (openrouter/openai/gpt-5.6-luna, thinking: low) Reviewed by @weareikko/code-review v0.9.5 for commit b0464b6. |
| applied = true; | ||
| apply(); | ||
| } | ||
| Promise.resolve(runner(applyOnce)).catch((error) => { |
There was a problem hiding this comment.
issue: Synchronous runner throws escape without applying
Because runner(applyOnce) is evaluated before Promise.resolve() receives its argument, a runner that throws synchronously escapes __applyIfBinding; the rejection handler never runs, so the DOM change is lost and the reactive update can fail. Invoke the runner inside a promise or try/catch so synchronous throws follow the same warning-and-apply fallback as rejected promises.
Confidence: high.
Reviewed by @weareikko/code-review v0.9.5 for commit b0464b6.
Export sizeBundled per export with peer dependencies left external, dynamic imports excluded and the output minified; sizes are gzipped. @studiometa/ui
Unchanged (117)@studiometa/ui
@studiometa/ui-mapbox
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #634 +/- ##
============================================
+ Coverage 87.17% 87.22% +0.05%
Complexity 145 145
============================================
Files 146 147 +1
Lines 5083 5121 +38
Branches 957 964 +7
============================================
+ Hits 4431 4467 +36
- Misses 569 571 +2
Partials 83 83
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
wrap((apply) => ...) says what the listener does — wrap the DOM change in its own transition runner — where through only described the data path. Mirrors the same rename on the Fetch seam. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
| apply(); | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
issue: Catch synchronously thrown runners and apply the change
Promise.resolve(runner(applyOnce)) evaluates runner(applyOnce) before Promise.resolve is called, so a runner that throws synchronously escapes __applyIfBinding instead of reaching the rejection handler. This breaks the guaranteed DOM-change fallback and can abort the reactive update; invoke the runner inside a try/Promise.resolve().then(...) boundary so synchronous throws are warned and applyOnce() still runs.
Confidence: high.
Reviewed by @weareikko/code-review v0.9.5 for commit e91aa86.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Satisfies the repo's func-style lint rule; identical commit on both branches carrying the shared helper so the file stays byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
What
The
data-bind:ifvirtual binding announces its DOM change with the shareddom-updateprotocol event: before it inserts or removes the<template>content,DataBindemits a bubblingdom-updateevent whosedetailcarries the new logical state asisPresentand awrap(runner)function. A listener can callwrap()to substitute what runs the DOM change — either a function receiving anapply()callback that performs the actual insertion or removal, or a duck-typed transitioner exposingupdate(mutate)(likeMotionViewfrom@studiometa/ui-motion) whoseupdate()receives the callback. The event name and helpers are shared across components — a parallel PR adopts the same protocol inFetch— through the newpackages/ui/src/utils/dom-update.tsmodule, which also exports theDomUpdateRunnerandDomUpdateTransitionerpublic types. This is the precedent set by the Dialogevent.detail.waitUntil()seam (#627), adapted to a DOM change that must run exactly once.Guardrails:
wrap()is only valid synchronously while the event dispatches — later calls warn and are ignored.wrap()call wins.$warnbeforeapply()runs anyway if the runner did not call it. The reactive pipeline stays synchronous — the runner is invoked fire-and-forget.Rapid-toggle consistency: the logical presence is tracked by a synchronous
__ifPresentflag, set before the event dispatches, so consecutive toggles make correct decisions even while a deferring runner is still pending. Toggling to the same logical state emits nothing, matching the previous no-op behavior. Eachapply()closure guards on__ifNodes(do nothing if the insertion already exists, or if the removal already ran), so queued runners applying in sequence keep the bookkeeping consistent — no duplicate insertions, no orphan nodes.The payoff is exit animations for removed template content: the nodes stay in the DOM until the runner calls
apply(), so a view-transition wrapper can animate them out — something the previous synchronous removal made impossible. Because the event bubbles, the upcoming ambientMotionViewwraps anydom-updateannounced in its subtree with zero wiring:For cross-subtree topologies, an ancestor
Actioncan still catch and route the event across the page, the same pattern as theTimerevents:Test plan
DataBindspecs pass untouched — no listener means the synchronous insert/remove behavior is unchanged.{ isPresent, wrap }with a runner deferring the insertion untilapply(); a duck-typed transitioner registered aswrap({ update })receiving the apply function; removed content staying in the DOM until the runner applies (the exit-animation enabler); latewrap()calls warning with the protocol message and being ignored; a rejecting runner warning and still applying the change; rapid toggles with a deferring runner keeping__ifNodesconsistent and same-state toggles emitting nothing.node scripts/validate-reference.ts→ Documentation validation passed (62 entries, 261 symbols, 5 concepts).npm run lint→ 0 errors, 21 warnings (the 19-warning baseline plus 2func-stylewarnings from the shareddom-update.tsmodule, kept byte-identical with the parallel Fetch PR).npm run test→ 98 files, 809 passed, 3 skipped, 1 todo.🤖 Generated with Claude Code
https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8