Skip to content

Announce the data-bind:if change with the wrappable dom-update protocol event - #634

Merged
titouanmathis merged 10 commits into
mainfrom
feature/data-bind-if-through
Aug 13, 2026
Merged

Announce the data-bind:if change with the wrappable dom-update protocol event#634
titouanmathis merged 10 commits into
mainfrom
feature/data-bind-if-through

Conversation

@titouanmathis

@titouanmathis titouanmathis commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

The data-bind:if virtual binding announces its DOM change with the shared dom-update protocol event: before it inserts or removes the <template> content, DataBind emits a bubbling dom-update event whose detail carries the new logical state as isPresent and a wrap(runner) function. A listener can call wrap() to substitute what runs the DOM change — either a function receiving an apply() callback that performs the actual insertion or removal, or a duck-typed transitioner exposing update(mutate) (like MotionView from @studiometa/ui-motion) whose update() receives the callback. The event name and helpers are shared across components — a parallel PR adopts the same protocol in Fetch — through the new packages/ui/src/utils/dom-update.ts module, which also exports the DomUpdateRunner and DomUpdateTransitioner public types. This is the precedent set by the Dialog event.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.
  • A single runner runs the change, the last wrap() call wins.
  • The DOM change is never lost: without a runner it runs synchronously as before (zero regression), and a rejecting runner is reported with $warn before apply() 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 __ifPresent flag, 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. Each apply() 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 ambient MotionView wraps any dom-update announced in its subtree with zero wiring:

<div data-component="MotionView">
  <template data-component="DataBind" data-option-key="query" data-bind:if="value !== ''"></template>
</div>

For cross-subtree topologies, an ancestor Action can still catch and route the event across the page, the same pattern as the Timer events:

<template
  data-component="Action DataBind"
  data-option-key="query"
  data-on:dom-update="MotionView(#panel)->event.detail.wrap(target)"
  data-bind:if="value !== ''"></template>

Test plan

  • All 41 pre-existing DataBind specs pass untouched — no listener means the synchronous insert/remove behavior is unchanged.
  • Specs cover: the event bubbles and carries { isPresent, wrap } with a runner deferring the insertion until apply(); a duck-typed transitioner registered as wrap({ update }) receiving the apply function; removed content staying in the DOM until the runner applies (the exit-animation enabler); late wrap() 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 __ifNodes consistent 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 2 func-style warnings from the shared dom-update.ts module, 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

titouanmathis and others added 5 commits August 13, 2026 09:20
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
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

Code Review

Risk: Low — The change adds the bubbling dom-update protocol event and wrapping support for data-bind:if while preserving synchronous behavior when no runner is registered. The implementation also handles deferred insertion and removal, rapid toggles, transitioner-style runners, late registrations, and rejected runners with test coverage. No blocking defects were found.

Still open from earlier reviews (1 finding):

  • packages/ui/src/Data/DataBind.ts:396issue: Catch synchronously thrown runners and apply the change

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 runs

Previous run archived 2026-08-13T12:11:14Z

Code Review

Risk: Low — The change adds the shared bubbling dom-update protocol to data-bind:if and documents and tests its synchronous wrapping and deferred DOM mutation behavior. No concrete defects are demonstrable from the provided diff.

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):

  • packages/ui/src/Data/DataBind.ts:396issue: Catch synchronously thrown runners and apply the change

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:18Z

Code Review

Risk: Medium — issues that should be addressed before merge.

Adds a bubbling bind-if event around virtual template insertion and removal, allowing listeners to defer or wrap the DOM mutation. The implementation tracks logical presence synchronously, guards queued applications, documents the new seam, and adds coverage for deferred, rejected, late, and rapid-toggle cases.

1 issue found:

  • issuepackages/ui/src/Data/DataBind.ts:414 — Catch synchronously thrown runners and apply the change

Still open from earlier reviews (1 finding):

  • packages/ui/src/Data/DataBind.ts:420issue: Synchronous runner throws escape without applying

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:52Z

Code Review

Risk: Medium — issues that should be addressed before merge.

Adds a bubbling bind-if event around virtual template insertion and removal, allowing listeners to defer or wrap the DOM mutation. Documentation and coverage are added for deferred changes, late registration, rejected runners, and rapid toggles.

1 issue found:

  • issuepackages/ui/src/Data/DataBind.ts:420 — Synchronous runner throws escape without applying

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.

Comment thread packages/ui/src/Data/DataBind.ts Outdated
applied = true;
apply();
}
Promise.resolve(runner(applyOnce)).catch((error) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

Export size

Bundled per export with peer dependencies left external, dynamic imports excluded and the output minified; sizes are gzipped.

@studiometa/ui

Export Size (gzip) Diff
DataEffect 4.04 kB +271 B (+7.0%)
DataModel 4.07 kB +271 B (+7.0%)
DataBind 3.97 kB +270 B (+7.1%)
DataComputed 4.06 kB +269 B (+6.9%)
(barrel) 30.76 kB +198 B (+0.6%)
manifest 2.99 kB -1 B (-0.0%)
Unchanged (117)

@studiometa/ui

Export Size (gzip) Diff
AbstractCarouselChild 542 B
AbstractCarouselComponent 219 B
AbstractFrameTrigger 1.11 kB
AbstractPrefetch 438 B
AbstractScrollAnimation 651 B
AbstractSliderChild 489 B
Accordion 1.11 kB
AccordionItem 995 B
Action 1.22 kB
AnchorNav 1018 B
AnchorNavLink 867 B
AnchorNavTarget 166 B
AnchorScrollTo 222 B
Carousel 2.89 kB
CarouselBtn 720 B
CarouselDrag 735 B
CarouselItem 786 B
CarouselWrapper 670 B
CircularMarquee 313 B
ClickOutside 219 B
Cursor 676 B
DataScope 1.4 kB
Dialog 1.75 kB
Disclosure 2.39 kB
DisclosureGroup 1.16 kB
Draggable 1.14 kB
Fetch 2.05 kB
FetchShopifyPartial 2.58 kB
FetchShopifySection 2.31 kB
Figure 1.07 kB
FigureShopify 1.35 kB
FigureTwicpics 1.6 kB
FigureVideo 1.27 kB
FigureVideoTwicpics 1.85 kB
Frame 2.51 kB
FrameAnchor 1.2 kB
FrameForm 1.32 kB
FrameLoader 788 B
FrameTarget 1.1 kB
FrameTriggerLoader 801 B
Hoverable 624 B
InView 204 B
InViewOnce 245 B
Indexable 1022 B
LargeText 499 B
LazyInclude 421 B
Menu 1.72 kB
MenuBtn 186 B
MenuList 1.28 kB
Modal 1.13 kB
ModalWithTransition 1.26 kB
Panel 1.52 kB
PrefetchWhenOver 485 B
PrefetchWhenVisible 510 B
ScrollAnimation 806 B
ScrollAnimationChild 979 B
ScrollAnimationChildWithEase 1.25 kB
ScrollAnimationParent 1.05 kB
ScrollAnimationTarget 898 B
ScrollAnimationTimeline 983 B
ScrollAnimationWithEase 1.08 kB
ScrollReveal 1005 B
Sentinel 175 B
Slider 2.28 kB
SliderBtn 745 B
SliderCount 549 B
SliderDots 1.15 kB
SliderDrag 348 B
SliderItem 574 B
SliderProgress 612 B
Sticky 929 B
Tabs 663 B
Target 125 B
Timer 589 B
TimerProgress 731 B
Toast 968 B
Toaster 1.3 kB
Track 1.61 kB
TrackContext 416 B
TrackShopify 1.71 kB
Transition 746 B
ViewTransition 658 B
animationScrollWithEase 438 B
autoload 3.03 kB
scheduler 324 B
types 20 B
viewTransition 325 B
withDeprecation 250 B
withIndex 986 B
withScrollAnimationDebug 1.63 kB
withTransition 717 B

@studiometa/ui-mapbox

Export Size (gzip) Diff
(barrel) 6.41 kB
AbstractMapboxControl 1.02 kB
AbstractMapboxMapChild 848 B
MAPBOX_CLUSTER_CONNECTED 143 B
MAPBOX_MAP_CONNECTED 128 B
MapboxCluster 2.13 kB
MapboxClusterItem 644 B
MapboxFullscreenControl 1.17 kB
MapboxGeocoder 1.21 kB
MapboxGeolocateControl 1.23 kB
MapboxImage 1.34 kB
MapboxImages 1.38 kB
MapboxLayer 1.36 kB
MapboxMap 723 B
MapboxMarker 1.11 kB
MapboxNavigationControl 1.21 kB
MapboxPopup 1.17 kB
MapboxSource 1.32 kB
StoreLocator 1.96 kB
autoload 681 B
dependencies 342 B
manifest 639 B
provideMapboxGeocoder 107 B
provideMapboxGl 104 B
resolveMapboxGeocoder 148 B
resolveMapboxGl 139 B

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.22%. Comparing base (11bd98a) to head (ac6c541).

Files with missing lines Patch % Lines
packages/ui/src/Data/DataBind.ts 92.00% 2 Missing ⚠️
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              
Flag Coverage Δ
unittests 87.22% <96.00%> (+0.05%) ⬆️

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

Files with missing lines Coverage Δ
packages/ui/src/utils/dom-update.ts 100.00% <100.00%> (ø)
packages/ui/src/Data/DataBind.ts 99.08% <92.00%> (-0.92%) ⬇️
🚀 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.

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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

titouanmathis and others added 3 commits August 13, 2026 14:04
@titouanmathis titouanmathis changed the title Emit a wrappable bind-if event around the data-bind:if change Announce the data-bind:if change with the wrappable dom-update protocol event Aug 13, 2026
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
@titouanmathis
titouanmathis merged commit 0872e90 into main Aug 13, 2026
20 checks passed
@titouanmathis
titouanmathis deleted the feature/data-bind-if-through branch August 13, 2026 12:43
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