ripple 0.3.128 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/package.json +10 -4
  3. package/src/runtime/element.js +23 -6
  4. package/src/runtime/index-client.js +81 -41
  5. package/src/runtime/index-server.js +1 -0
  6. package/src/runtime/internal/client/blocks.js +173 -91
  7. package/src/runtime/internal/client/component.js +4 -30
  8. package/src/runtime/internal/client/composite.js +12 -13
  9. package/src/runtime/internal/client/constants.js +9 -0
  10. package/src/runtime/internal/client/context.js +12 -19
  11. package/src/runtime/internal/client/css.js +7 -9
  12. package/src/runtime/internal/client/events.js +178 -90
  13. package/src/runtime/internal/client/expression.js +45 -25
  14. package/src/runtime/internal/client/for.js +409 -129
  15. package/src/runtime/internal/client/head.js +10 -5
  16. package/src/runtime/internal/client/html.js +1 -0
  17. package/src/runtime/internal/client/hydration.js +152 -10
  18. package/src/runtime/internal/client/if.js +239 -50
  19. package/src/runtime/internal/client/index.js +4 -12
  20. package/src/runtime/internal/client/operations.js +40 -61
  21. package/src/runtime/internal/client/portal.js +163 -85
  22. package/src/runtime/internal/client/render.js +39 -39
  23. package/src/runtime/internal/client/rpc.js +3 -3
  24. package/src/runtime/internal/client/runtime.js +592 -433
  25. package/src/runtime/internal/client/selector.js +5 -2
  26. package/src/runtime/internal/client/template.js +133 -158
  27. package/src/runtime/internal/client/transport.js +38 -0
  28. package/src/runtime/internal/client/try.js +465 -426
  29. package/src/runtime/internal/client/types.d.ts +56 -23
  30. package/src/runtime/internal/client/utils.js +1 -3
  31. package/src/runtime/internal/server/dynamic.js +20 -21
  32. package/src/runtime/internal/server/index.js +325 -275
  33. package/src/runtime/internal/server/rpc.js +3 -2
  34. package/src/runtime/internal/server/transport.js +16 -0
  35. package/src/runtime/transport.js +21 -0
  36. package/tests/client/__snapshots__/for.test.tsrx.snap +0 -5
  37. package/tests/client/array/array.copy-within.test.tsrx +12 -12
  38. package/tests/client/array/array.derived.test.tsrx +46 -46
  39. package/tests/client/array/array.iteration.test.tsrx +10 -10
  40. package/tests/client/array/array.mutations.test.tsrx +20 -20
  41. package/tests/client/array/array.static.test.tsrx +11 -11
  42. package/tests/client/array/array.to-methods.test.tsrx +6 -6
  43. package/tests/client/async-suspend.test.tsrx +74 -75
  44. package/tests/client/basic/basic.attributes.test.tsrx +86 -83
  45. package/tests/client/basic/basic.collections.test.tsrx +12 -12
  46. package/tests/client/basic/basic.components.test.tsrx +99 -25
  47. package/tests/client/basic/basic.errors.test.tsrx +18 -18
  48. package/tests/client/basic/basic.events.test.tsrx +42 -42
  49. package/tests/client/basic/basic.get-set.test.tsrx +8 -8
  50. package/tests/client/basic/basic.reactivity.test.tsrx +73 -38
  51. package/tests/client/basic/basic.rendering.test.tsrx +25 -25
  52. package/tests/client/basic/basic.utilities.test.tsrx +3 -3
  53. package/tests/client/boundaries.test.tsrx +8 -8
  54. package/tests/client/boxed-locals.test.tsrx +452 -0
  55. package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +6 -6
  56. package/tests/client/compiler/compiler.assignments.test.tsrx +9 -73
  57. package/tests/client/compiler/compiler.attributes.test.tsrx +23 -23
  58. package/tests/client/compiler/compiler.basic.test.tsrx +15 -60
  59. package/tests/client/compiler/compiler.tracked-access.test.tsrx +18 -43
  60. package/tests/client/compiler/compiler.try-in-function.test.tsrx +2 -2
  61. package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -1
  62. package/tests/client/composite/composite.dynamic-components.test.tsrx +77 -6
  63. package/tests/client/composite/composite.props.test.tsrx +149 -12
  64. package/tests/client/composite/composite.reactivity.test.tsrx +51 -52
  65. package/tests/client/composite/composite.render.test.tsrx +26 -9
  66. package/tests/client/css/scoped-styles.test.tsrx +8 -8
  67. package/tests/client/date.test.tsrx +42 -42
  68. package/tests/client/derived-release.test.tsrx +370 -0
  69. package/tests/client/dynamic-elements.test.tsrx +45 -45
  70. package/tests/client/events.test.tsrx +97 -71
  71. package/tests/client/for-selector.test.tsrx +14 -14
  72. package/tests/client/for-single-root-items.test.tsrx +118 -3
  73. package/tests/client/for.test.tsrx +332 -31
  74. package/tests/client/head.test.tsrx +19 -19
  75. package/tests/client/html.test.tsrx +11 -9
  76. package/tests/client/if-block.test.tsrx +80 -0
  77. package/tests/client/map.test.tsrx +16 -16
  78. package/tests/client/media-query.test.tsrx +7 -7
  79. package/tests/client/portal.test.tsrx +224 -26
  80. package/tests/client/primitive-text.test.tsrx +50 -0
  81. package/tests/client/prop-kind-switch.test.tsrx +141 -0
  82. package/tests/client/ref.test.tsrx +17 -17
  83. package/tests/client/return.test.tsrx +26 -3
  84. package/tests/client/root-boundary.test.tsrx +42 -0
  85. package/tests/client/scoped-flush.test.tsrx +8 -8
  86. package/tests/client/set.test.tsrx +6 -6
  87. package/tests/client/svg.test.tsrx +11 -11
  88. package/tests/client/switch.test.tsrx +44 -44
  89. package/tests/client/track-async-hydration.test.tsrx +4 -4
  90. package/tests/client/try.test.tsrx +151 -83
  91. package/tests/client/tsx.test.tsrx +43 -40
  92. package/tests/client/url/url.derived.test.tsrx +6 -6
  93. package/tests/client/url-search-params/url-search-params.derived.test.tsrx +8 -8
  94. package/tests/client/url-search-params/url-search-params.iteration.test.tsrx +10 -10
  95. package/tests/client/url-search-params/url-search-params.mutation.test.tsrx +10 -10
  96. package/tests/client/url-search-params/url-search-params.retrieval.test.tsrx +18 -18
  97. package/tests/client/url-search-params/url-search-params.serialization.test.tsrx +2 -2
  98. package/tests/hydration/basic.test.js +74 -1
  99. package/tests/hydration/build-components.js +22 -1
  100. package/tests/hydration/compiled/client/basic.js +1142 -713
  101. package/tests/hydration/compiled/client/composite.js +151 -100
  102. package/tests/hydration/compiled/client/events.js +184 -171
  103. package/tests/hydration/compiled/client/for.js +1472 -1132
  104. package/tests/hydration/compiled/client/fragment-cursor.js +2341 -0
  105. package/tests/hydration/compiled/client/head.js +254 -201
  106. package/tests/hydration/compiled/client/hmr.js +54 -41
  107. package/tests/hydration/compiled/client/html-in-template.js +54 -39
  108. package/tests/hydration/compiled/client/html.js +1633 -1263
  109. package/tests/hydration/compiled/client/if-children.js +420 -270
  110. package/tests/hydration/compiled/client/if-fragment-controlflow.js +363 -317
  111. package/tests/hydration/compiled/client/if.js +229 -219
  112. package/tests/hydration/compiled/client/mixed-control-flow.js +420 -404
  113. package/tests/hydration/compiled/client/nested-control-flow.js +1326 -1277
  114. package/tests/hydration/compiled/client/portal.js +87 -95
  115. package/tests/hydration/compiled/client/reactivity.js +135 -101
  116. package/tests/hydration/compiled/client/return.js +28 -19
  117. package/tests/hydration/compiled/client/streaming.js +390 -306
  118. package/tests/hydration/compiled/client/switch.js +258 -266
  119. package/tests/hydration/compiled/client/track-async-serialization.js +448 -280
  120. package/tests/hydration/compiled/client/try.js +173 -129
  121. package/tests/hydration/compiled/client/typed-text.js +71 -0
  122. package/tests/hydration/compiled/fixtures/money.js +10 -0
  123. package/tests/hydration/compiled/server/basic.js +123 -6
  124. package/tests/hydration/compiled/server/composite.js +16 -30
  125. package/tests/hydration/compiled/server/events.js +66 -18
  126. package/tests/hydration/compiled/server/for.js +117 -41
  127. package/tests/hydration/compiled/server/fragment-cursor.js +1687 -0
  128. package/tests/hydration/compiled/server/head.js +20 -20
  129. package/tests/hydration/compiled/server/hmr.js +2 -2
  130. package/tests/hydration/compiled/server/html.js +70 -238
  131. package/tests/hydration/compiled/server/if-children.js +107 -15
  132. package/tests/hydration/compiled/server/if.js +11 -15
  133. package/tests/hydration/compiled/server/mixed-control-flow.js +8 -8
  134. package/tests/hydration/compiled/server/portal.js +14 -22
  135. package/tests/hydration/compiled/server/reactivity.js +14 -14
  136. package/tests/hydration/compiled/server/streaming.js +19 -19
  137. package/tests/hydration/compiled/server/switch.js +8 -8
  138. package/tests/hydration/compiled/server/track-async-serialization.js +150 -23
  139. package/tests/hydration/compiled/server/try.js +8 -8
  140. package/tests/hydration/compiled/server/typed-text.js +17 -0
  141. package/tests/hydration/components/basic.tsrx +44 -8
  142. package/tests/hydration/components/composite.tsrx +2 -2
  143. package/tests/hydration/components/events.tsrx +25 -25
  144. package/tests/hydration/components/for.tsrx +115 -67
  145. package/tests/hydration/components/fragment-cursor.tsrx +650 -0
  146. package/tests/hydration/components/head.tsrx +16 -16
  147. package/tests/hydration/components/hmr.tsrx +2 -2
  148. package/tests/hydration/components/html.tsrx +7 -7
  149. package/tests/hydration/components/if-children.tsrx +72 -25
  150. package/tests/hydration/components/if.tsrx +18 -18
  151. package/tests/hydration/components/mixed-control-flow.tsrx +11 -11
  152. package/tests/hydration/components/portal.tsrx +3 -3
  153. package/tests/hydration/components/reactivity.tsrx +16 -16
  154. package/tests/hydration/components/streaming.tsrx +20 -20
  155. package/tests/hydration/components/switch.tsrx +20 -20
  156. package/tests/hydration/components/text-types.ts +4 -0
  157. package/tests/hydration/components/track-async-serialization.tsrx +80 -26
  158. package/tests/hydration/components/try.tsrx +8 -8
  159. package/tests/hydration/components/typed-text.tsrx +22 -0
  160. package/tests/hydration/fixtures/money.js +10 -0
  161. package/tests/hydration/for.test.js +36 -0
  162. package/tests/hydration/fragment-cursor.test.js +111 -0
  163. package/tests/hydration/if-children.test.js +24 -0
  164. package/tests/hydration/streaming.test.js +19 -0
  165. package/tests/hydration/track-async-serialization.test.js +87 -2
  166. package/tests/hydration/tsconfig.text-types.json +10 -0
  167. package/tests/hydration/typed-text.test.js +34 -0
  168. package/tests/server/await.test.tsrx +3 -3
  169. package/tests/server/basic.attributes.test.tsrx +42 -42
  170. package/tests/server/basic.components.test.tsrx +64 -8
  171. package/tests/server/basic.test.tsrx +34 -32
  172. package/tests/server/compiler.test.tsrx +1 -70
  173. package/tests/server/composite.props.test.tsrx +6 -6
  174. package/tests/server/dynamic-elements.test.tsrx +31 -8
  175. package/tests/server/for.test.tsrx +94 -0
  176. package/tests/server/head.test.tsrx +11 -11
  177. package/tests/server/scoped-styles.test.tsrx +2 -2
  178. package/tests/server/streaming-ssr.test.tsrx +48 -43
  179. package/tests/server/track-async-serialization.test.tsrx +204 -21
  180. package/tests/server/try.test.tsrx +48 -48
  181. package/tests/types/transport.ts +40 -0
  182. package/tests/utils/vite-plugin-config.test.js +23 -0
  183. package/types/index.d.ts +48 -7
  184. package/types/server.d.ts +3 -0
  185. package/types/transport.d.ts +15 -0
  186. package/src/runtime/internal/client/switch.js +0 -113
  187. package/src/utils/errors.js +0 -13
  188. package/tests/client/lazy-array.test.tsrx +0 -31
  189. package/tests/client/lazy-destructuring.test.tsrx +0 -438
  190. package/tests/client/tracked-index-access.test.tsrx +0 -113
  191. package/tests/server/lazy-destructuring.test.tsrx +0 -595
  192. package/tests/server/tracked-index-access.test.tsrx +0 -76
@@ -1,20 +1,20 @@
1
1
  import type { OnEventListenerRemover } from 'ripple';
2
- import { effect, flushSync, on, Portal, track } from 'ripple';
2
+ import { effect, flushSync, mount, on, Portal, track } from 'ripple';
3
3
 
4
4
  describe('on() event handler', () => {
5
5
  it('should attach multiple handlers via onClick attribute (delegated)', () => {
6
6
  function Basic() @{
7
- let &[count1] = track(0);
8
- let &[count2] = track(0);
7
+ const count1 = track(0);
8
+ const count2 = track(0);
9
9
  <>
10
10
  <button
11
11
  onClick={() => {
12
- count1++;
13
- count2++;
12
+ count1.value++;
13
+ count2.value++;
14
14
  }}
15
15
  >{'Click me'}</button>
16
- <div class="count1">{count1}</div>
17
- <div class="count2">{count2}</div>
16
+ <div class="count1">{count1.value}</div>
17
+ <div class="count2">{count2.value}</div>
18
18
  </>
19
19
  }
20
20
 
@@ -67,16 +67,16 @@ describe('on() event handler', () => {
67
67
 
68
68
  it('should attach and remove a single event handler', () => {
69
69
  function Basic() @{
70
- let &[count] = track(0);
70
+ const count = track(0);
71
71
  const setupListener = (node: HTMLButtonElement) => {
72
72
  const remove = on(node, 'click', () => {
73
- count++;
73
+ count.value++;
74
74
  });
75
75
  return remove;
76
76
  };
77
77
  <>
78
78
  <button ref={setupListener}>{'Click me'}</button>
79
- <div class="count">{count}</div>
79
+ <div class="count">{count.value}</div>
80
80
  </>
81
81
  }
82
82
 
@@ -99,14 +99,14 @@ describe('on() event handler', () => {
99
99
 
100
100
  it('should handle multiple different event types on same element', () => {
101
101
  function Basic() @{
102
- let &[clickCount] = track(0);
103
- let &[mousedownCount] = track(0);
102
+ const clickCount = track(0);
103
+ const mousedownCount = track(0);
104
104
  const setupListeners = (node: HTMLButtonElement) => {
105
105
  const remove1 = on(node, 'click', () => {
106
- clickCount++;
106
+ clickCount.value++;
107
107
  });
108
108
  const remove2 = on(node, 'mousedown', () => {
109
- mousedownCount++;
109
+ mousedownCount.value++;
110
110
  });
111
111
  return () => {
112
112
  remove1();
@@ -115,8 +115,8 @@ describe('on() event handler', () => {
115
115
  };
116
116
  <>
117
117
  <button ref={setupListeners}>{'Test'}</button>
118
- <div class="click-count">{clickCount}</div>
119
- <div class="mousedown-count">{mousedownCount}</div>
118
+ <div class="click-count">{clickCount.value}</div>
119
+ <div class="mousedown-count">{mousedownCount.value}</div>
120
120
  </>
121
121
  }
122
122
 
@@ -149,16 +149,16 @@ describe('on() event handler', () => {
149
149
 
150
150
  it('should handle multiple handlers for same event type on same element', () => {
151
151
  function Basic() @{
152
- let &[callOrder] = track<number[]>([]);
152
+ const callOrder = track<number[]>([]);
153
153
  const setupListeners = (node: HTMLButtonElement) => {
154
154
  const remove1 = on(node, 'click', () => {
155
- callOrder = [...callOrder, 1];
155
+ callOrder.value = [...callOrder.value, 1];
156
156
  });
157
157
  const remove2 = on(node, 'click', () => {
158
- callOrder = [...callOrder, 2];
158
+ callOrder.value = [...callOrder.value, 2];
159
159
  });
160
160
  const remove3 = on(node, 'click', () => {
161
- callOrder = [...callOrder, 3];
161
+ callOrder.value = [...callOrder.value, 3];
162
162
  });
163
163
  return () => {
164
164
  remove1();
@@ -168,7 +168,7 @@ describe('on() event handler', () => {
168
168
  };
169
169
  <>
170
170
  <button ref={setupListeners}>{'Click me'}</button>
171
- <div class="order">{callOrder.join(',')}</div>
171
+ <div class="order">{callOrder.value.join(',')}</div>
172
172
  </>
173
173
  }
174
174
 
@@ -192,19 +192,19 @@ describe('on() event handler', () => {
192
192
 
193
193
  it('should remove specific handler without affecting others', () => {
194
194
  function Basic() @{
195
- let &[handler1Called] = track(0);
196
- let &[handler2Called] = track(0);
197
- let &[handler3Called] = track(0);
195
+ const handler1Called = track(0);
196
+ const handler2Called = track(0);
197
+ const handler3Called = track(0);
198
198
  let removeHandler2: OnEventListenerRemover | undefined;
199
199
  const setupListeners = (node: HTMLButtonElement) => {
200
200
  const remove1 = on(node, 'click', () => {
201
- handler1Called++;
201
+ handler1Called.value++;
202
202
  });
203
203
  removeHandler2 = on(node, 'click', () => {
204
- handler2Called++;
204
+ handler2Called.value++;
205
205
  });
206
206
  const remove3 = on(node, 'click', () => {
207
- handler3Called++;
207
+ handler3Called.value++;
208
208
  });
209
209
  return () => {
210
210
  remove1();
@@ -221,9 +221,9 @@ describe('on() event handler', () => {
221
221
  removeHandler2 = undefined;
222
222
  }}
223
223
  >{'Remove handler 2'}</button>
224
- <div class="h1">{handler1Called}</div>
225
- <div class="h2">{handler2Called}</div>
226
- <div class="h3">{handler3Called}</div>
224
+ <div class="h1">{handler1Called.value}</div>
225
+ <div class="h2">{handler2Called.value}</div>
226
+ <div class="h3">{handler3Called.value}</div>
227
227
  </div>
228
228
  }
229
229
 
@@ -266,17 +266,17 @@ describe('on() event handler', () => {
266
266
  'should handle change event with multiple handlers (like bindChecked and bindIndeterminate)',
267
267
  () => {
268
268
  function Basic() @{
269
- let &[checked] = track(false);
270
- let &[indeterminate] = track(true);
269
+ const checked = track(false);
270
+ const indeterminate = track(true);
271
271
  const setupListeners = (node: HTMLInputElement) => {
272
- node.indeterminate = indeterminate;
273
- node.checked = checked;
272
+ node.indeterminate = indeterminate.value;
273
+ node.checked = checked.value;
274
274
 
275
275
  const remove1 = on(node, 'change', () => {
276
- checked = node.checked;
276
+ checked.value = node.checked;
277
277
  });
278
278
  const remove2 = on(node, 'change', () => {
279
- indeterminate = node.indeterminate;
279
+ indeterminate.value = node.indeterminate;
280
280
  });
281
281
  return () => {
282
282
  remove1();
@@ -286,10 +286,10 @@ describe('on() event handler', () => {
286
286
  <div>
287
287
  <input type="checkbox" ref={setupListeners} />
288
288
  <div class="checked">
289
- {checked ? 'true' : 'false'}
289
+ {checked.value ? 'true' : 'false'}
290
290
  </div>
291
291
  <div class="indeterminate">
292
- {indeterminate ? 'true' : 'false'}
292
+ {indeterminate.value ? 'true' : 'false'}
293
293
  </div>
294
294
  </div>
295
295
  }
@@ -317,16 +317,16 @@ describe('on() event handler', () => {
317
317
 
318
318
  it('should support non-delegated events', () => {
319
319
  function Basic() @{
320
- let &[focusCount] = track(0);
320
+ const focusCount = track(0);
321
321
  const setupListener = (node: HTMLInputElement) => {
322
322
  const remove = on(node, 'focus', () => {
323
- focusCount++;
323
+ focusCount.value++;
324
324
  });
325
325
  return remove;
326
326
  };
327
327
  <>
328
328
  <input ref={setupListener} />
329
- <div class="focus-count">{focusCount}</div>
329
+ <div class="focus-count">{focusCount.value}</div>
330
330
  </>
331
331
  }
332
332
 
@@ -349,19 +349,19 @@ describe('on() event handler', () => {
349
349
 
350
350
  it('should handle removal of all handlers for same event type', () => {
351
351
  function Basic() @{
352
- let &[count] = track(0);
352
+ const count = track(0);
353
353
  let remove1: OnEventListenerRemover | undefined;
354
354
  let remove2: OnEventListenerRemover | undefined;
355
355
  let remove3: OnEventListenerRemover | undefined;
356
356
  const setupListeners = (node: HTMLButtonElement) => {
357
357
  remove1 = on(node, 'click', () => {
358
- count++;
358
+ count.value++;
359
359
  });
360
360
  remove2 = on(node, 'click', () => {
361
- count += 10;
361
+ count.value += 10;
362
362
  });
363
363
  remove3 = on(node, 'click', () => {
364
- count += 100;
364
+ count.value += 100;
365
365
  });
366
366
  return () => {
367
367
  remove1?.();
@@ -380,7 +380,7 @@ describe('on() event handler', () => {
380
380
  remove1 = remove2 = remove3 = undefined;
381
381
  }}
382
382
  >{'Remove all'}</button>
383
- <div class="count">{count}</div>
383
+ <div class="count">{count.value}</div>
384
384
  </div>
385
385
  }
386
386
 
@@ -410,9 +410,9 @@ describe('on() event handler', () => {
410
410
 
411
411
  it('should not add duplicate handlers when same handler is attached multiple times', () => {
412
412
  function Basic() @{
413
- let &[count] = track(0);
413
+ const count = track(0);
414
414
  const sharedHandler = () => {
415
- count++;
415
+ count.value++;
416
416
  };
417
417
  const setupListeners = (node: HTMLButtonElement) => {
418
418
  // Attach the same handler multiple times
@@ -428,7 +428,7 @@ describe('on() event handler', () => {
428
428
  };
429
429
  <>
430
430
  <button ref={setupListeners}>{'Click me'}</button>
431
- <div class="count">{count}</div>
431
+ <div class="count">{count.value}</div>
432
432
  </>
433
433
  }
434
434
 
@@ -452,9 +452,9 @@ describe('on() event handler', () => {
452
452
 
453
453
  it('should allow duplicate handlers when delegated is false (no deduplication)', () => {
454
454
  function Basic() @{
455
- let &[count] = track(0);
455
+ const count = track(0);
456
456
  const sharedHandler = () => {
457
- count++;
457
+ count.value++;
458
458
  };
459
459
  const setupListeners = (node: HTMLButtonElement) => {
460
460
  // Attach the same handler multiple times with delegated: false
@@ -470,7 +470,7 @@ describe('on() event handler', () => {
470
470
  };
471
471
  <>
472
472
  <button ref={setupListeners}>{'Click me'}</button>
473
- <div class="count">{count}</div>
473
+ <div class="count">{count.value}</div>
474
474
  </>
475
475
  }
476
476
 
@@ -495,12 +495,12 @@ describe('on() event handler', () => {
495
495
 
496
496
  it('should fire capture event on parent before bubbling event on child', () => {
497
497
  function Basic() @{
498
- let &[callOrder] = track<string[]>([]);
498
+ const callOrder = track<string[]>([]);
499
499
  const parentCaptureHandler = () => {
500
- callOrder = [...callOrder, 'parent-capture'];
500
+ callOrder.value = [...callOrder.value, 'parent-capture'];
501
501
  };
502
502
  const childBubbleHandler = () => {
503
- callOrder = [...callOrder, 'child-bubble'];
503
+ callOrder.value = [...callOrder.value, 'child-bubble'];
504
504
  };
505
505
  const setupParent = (node: HTMLDivElement) => {
506
506
  return on(node, 'clickCapture', parentCaptureHandler);
@@ -512,7 +512,7 @@ describe('on() event handler', () => {
512
512
  <div ref={setupParent} class="parent">
513
513
  <button ref={setupChild} class="child">{'Click me'}</button>
514
514
  </div>
515
- <div class="order">{callOrder.join(',')}</div>
515
+ <div class="order">{callOrder.value.join(',')}</div>
516
516
  </>
517
517
  }
518
518
 
@@ -537,15 +537,15 @@ describe('on() event handler', () => {
537
537
 
538
538
  it('should fire handler only once when once option is true', () => {
539
539
  function Basic() @{
540
- let &[count] = track(0);
541
- let &[permanentCount] = track(0);
540
+ const count = track(0);
541
+ const permanentCount = track(0);
542
542
  const setupListeners = (node: HTMLButtonElement) => {
543
543
  const onceHandler = on(node, 'click', () => {
544
- count++;
544
+ count.value++;
545
545
  }, { once: true });
546
546
 
547
547
  const permanentHandler = on(node, 'click', () => {
548
- permanentCount++;
548
+ permanentCount.value++;
549
549
  });
550
550
 
551
551
  return () => {
@@ -555,8 +555,8 @@ describe('on() event handler', () => {
555
555
  };
556
556
  <>
557
557
  <button ref={setupListeners}>{'Click me'}</button>
558
- <div class="once-count">{count}</div>
559
- <div class="permanent-count">{permanentCount}</div>
558
+ <div class="once-count">{count.value}</div>
559
+ <div class="permanent-count">{permanentCount.value}</div>
560
560
  </>
561
561
  }
562
562
 
@@ -591,16 +591,16 @@ describe('on() event handler', () => {
591
591
 
592
592
  it('should handle click events on window', () => {
593
593
  function Basic() @{
594
- let &[windowClickCount] = track(0);
594
+ const windowClickCount = track(0);
595
595
  effect(() => {
596
596
  const removeWindowListener = on(window, 'click', () => {
597
- windowClickCount++;
597
+ windowClickCount.value++;
598
598
  });
599
599
  return removeWindowListener;
600
600
  });
601
601
  <div>
602
602
  <button class="test-btn">{'Click me'}</button>
603
- <div class="window-count">{windowClickCount}</div>
603
+ <div class="window-count">{windowClickCount.value}</div>
604
604
  </div>
605
605
  }
606
606
 
@@ -625,16 +625,16 @@ describe('on() event handler', () => {
625
625
 
626
626
  it('should handle click events on document', () => {
627
627
  function Basic() @{
628
- let &[documentClickCount] = track(0);
628
+ const documentClickCount = track(0);
629
629
  effect(() => {
630
630
  const removeDocumentListener = on(document, 'click', () => {
631
- documentClickCount++;
631
+ documentClickCount.value++;
632
632
  });
633
633
  return removeDocumentListener;
634
634
  });
635
635
  <div>
636
636
  <button class="test-btn">{'Click me'}</button>
637
- <div class="document-count">{documentClickCount}</div>
637
+ <div class="document-count">{documentClickCount.value}</div>
638
638
  </div>
639
639
  }
640
640
 
@@ -659,16 +659,16 @@ describe('on() event handler', () => {
659
659
 
660
660
  it('should handle click events on body', () => {
661
661
  function Basic() @{
662
- let &[bodyClickCount] = track(0);
662
+ const bodyClickCount = track(0);
663
663
  effect(() => {
664
664
  const removeBodyListener = on(document.body, 'click', () => {
665
- bodyClickCount++;
665
+ bodyClickCount.value++;
666
666
  });
667
667
  return removeBodyListener;
668
668
  });
669
669
  <div>
670
670
  <button class="test-btn">{'Click me'}</button>
671
- <div class="body-count">{bodyClickCount}</div>
671
+ <div class="body-count">{bodyClickCount.value}</div>
672
672
  </div>
673
673
  }
674
674
 
@@ -691,3 +691,29 @@ describe('on() event handler', () => {
691
691
  expect(bodyCountDiv.textContent).toBe('2');
692
692
  });
693
693
  });
694
+
695
+ describe('root event delegation lifecycle', () => {
696
+ it('ignores a repeated unmount() so a later mount on the same target keeps delegation', () => {
697
+ // The target's delegated listeners are ref-counted per mount/portal. A
698
+ // disposer called twice (remount, HMR, a stale reference) must not
699
+ // release the count a fresh mount now owns.
700
+ const target = document.createElement('div');
701
+
702
+ function Counter() @{
703
+ const count = track(0);
704
+ <button onClick={() => count.value++}>{count.value}</button>
705
+ }
706
+
707
+ const unmount_first = mount(Counter, { target });
708
+ unmount_first();
709
+
710
+ const unmount_second = mount(Counter, { target });
711
+ unmount_first();
712
+
713
+ target.querySelector('button').click();
714
+ flushSync();
715
+ expect(target.querySelector('button').textContent).toBe('1');
716
+
717
+ unmount_second();
718
+ });
719
+ });
@@ -37,33 +37,33 @@ describe('@for selector comparisons', () => {
37
37
  function App() @{
38
38
  const row = (id: number, group = 'a'): Row => ({ id, group, label: track(`row ${id}`) });
39
39
 
40
- let &[items] = track<Row[]>([row(1), row(2), row(3)]);
41
- let &[selected] = track<number | undefined>(undefined);
42
- let &[group] = track<string | undefined>(undefined);
40
+ const items = track<Row[]>([row(1), row(2), row(3)]);
41
+ const selected = track<number | undefined>(undefined);
42
+ const group = track<string | undefined>(undefined);
43
43
 
44
44
  actions = {
45
45
  select: (id) => {
46
- selected = id;
46
+ selected.value = id;
47
47
  },
48
48
  setItems: (specs) => {
49
- items = specs.map(([id, group]) => row(id, group));
49
+ items.value = specs.map(([id, group]) => row(id, group));
50
50
  },
51
51
  setGroup: (next) => {
52
- group = next;
52
+ group.value = next;
53
53
  },
54
54
  relabel: (index, label) => {
55
- items[index].label.value = label;
55
+ items.value[index].label.value = label;
56
56
  },
57
57
  };
58
58
 
59
59
  <ul>
60
- @for (const item of items; key item.id) {
60
+ @for (const item of items.value; key item.id) {
61
61
  <li
62
62
  data-id={item.id}
63
- class={selected === item.id ? 'selected' : ''}
64
- data-group={group === item.group ? 'match' : ''}
63
+ class={selected.value === item.id ? 'selected' : ''}
64
+ data-group={group.value === item.group ? 'match' : ''}
65
65
  >
66
- {selected !== item.id ? item.label.value : 'current'}
66
+ {selected.value !== item.id ? item.label.value : 'current'}
67
67
  </li>
68
68
  }
69
69
  </ul>
@@ -177,12 +177,12 @@ describe('keyed reconciliation with most items moving', () => {
177
177
  let apply: (order: number[]) => void = () => {};
178
178
 
179
179
  function App() @{
180
- let &[items] = track(Array.from({ length: 300 }, (_, i) => i));
180
+ const items = track(Array.from({ length: 300 }, (_, i) => i));
181
181
  apply = (order) => {
182
- items = order;
182
+ items.value = order;
183
183
  };
184
184
  <ul>
185
- @for (const item of items; key item) {
185
+ @for (const item of items.value; key item) {
186
186
  <li data-id={item}>{item}</li>
187
187
  }
188
188
  </ul>
@@ -1,4 +1,4 @@
1
- import { RippleArray, flushSync, track } from 'ripple';
1
+ import { RippleArray, flushSync, mount, track } from 'ripple';
2
2
 
3
3
  // Regression tests for the 0.3.85 optimization (PR #1307) that elides the
4
4
  // `<!>` wrapper for single control-flow / component root scopes. When such a
@@ -98,11 +98,11 @@ describe('keyed for with single-root item bodies (#1307 regression)', () => {
98
98
  }
99
99
  }
100
100
  function App() @{
101
- let &[loaded, loadedT] = track(true);
101
+ const loaded = track(true);
102
102
  const muzes = new RippleArray({ muzeId: 'b' }, { muzeId: 'c' });
103
103
  <>
104
104
  <button onClick={() => muzes.unshift({ muzeId: 'a' })}>{'add'}</button>
105
- @if (loaded) {
105
+ @if (loaded.value) {
106
106
  <>
107
107
  @for (const muze of muzes; key muze.muzeId) {
108
108
  <MuzeCard {muze} />
@@ -127,4 +127,119 @@ describe('keyed for with single-root item bodies (#1307 regression)', () => {
127
127
  ]);
128
128
  expect(container.querySelector('.count').textContent).toBe('3');
129
129
  });
130
+
131
+ describe('controlled list of root @if items (lazy anchors)', () => {
132
+ const text_nodes = (el) => Array.from(el.childNodes).filter((n) => n.nodeType === 3).length;
133
+
134
+ it('keeps no anchor nodes until a branch swaps, then removes the anchor with the item', () => {
135
+ function App() @{
136
+ const items = track([
137
+ { id: 1, code: false },
138
+ { id: 2, code: true },
139
+ { id: 3, code: false },
140
+ ]);
141
+ <>
142
+ <button
143
+ class="swap"
144
+ onClick={() => {
145
+ items.value = items.value.map((i) => (i.id === 2 ? { ...i, code: false } : i));
146
+ }}
147
+ >{'swap'}</button>
148
+ <button
149
+ class="remove"
150
+ onClick={() => {
151
+ items.value = items.value.filter((i) => i.id !== 2);
152
+ }}
153
+ >{'remove'}</button>
154
+ <button
155
+ class="clear"
156
+ onClick={() => {
157
+ items.value = [];
158
+ }}
159
+ >{'clear'}</button>
160
+ <ul>
161
+ @for (const item of items.value; key item.id) {
162
+ @if (item.code) {
163
+ <li class="code">{item.id}</li>
164
+ } @else {
165
+ <li class="text">{item.id}</li>
166
+ }
167
+ }
168
+ </ul>
169
+ </>
170
+ }
171
+ render(App);
172
+ const ul = container.querySelector('ul');
173
+ expect(text_nodes(ul)).toBe(0);
174
+ expect(Array.from(ul.children).map((li) => li.className)).toEqual(['text', 'code', 'text']);
175
+
176
+ container.querySelector('.swap').click();
177
+ flushSync();
178
+ expect(Array.from(ul.children).map((li) => li.className)).toEqual(['text', 'text', 'text']);
179
+ // The swapped item now owns one text anchor, placed right after it.
180
+ expect(text_nodes(ul)).toBe(1);
181
+ expect(ul.children[1].nextSibling.nodeType).toBe(3);
182
+
183
+ container.querySelector('.remove').click();
184
+ flushSync();
185
+ expect(Array.from(ul.children).map((li) => li.textContent)).toEqual(['1', '3']);
186
+ expect(text_nodes(ul)).toBe(0);
187
+
188
+ container.querySelector('.clear').click();
189
+ flushSync();
190
+ expect(ul.childNodes.length).toBe(0);
191
+ });
192
+
193
+ it('moves a materialized anchor with its item and removes it on unmount', () => {
194
+ function App() @{
195
+ const items = track([
196
+ { id: 1, show: false },
197
+ { id: 2, show: true },
198
+ ]);
199
+ <>
200
+ <button
201
+ class="show"
202
+ onClick={() => {
203
+ items.value = items.value.map((i) => (i.id === 1 ? { ...i, show: true } : i));
204
+ }}
205
+ >{'show'}</button>
206
+ <button
207
+ class="reverse"
208
+ onClick={() => {
209
+ items.value = [...items.value].reverse();
210
+ }}
211
+ >{'reverse'}</button>
212
+ <ul>
213
+ @for (const item of items.value; key item.id) {
214
+ @if (item.show) {
215
+ <li>{item.id}</li>
216
+ }
217
+ }
218
+ </ul>
219
+ </>
220
+ }
221
+ const unmount = mount(App, { target: container, rootBoundary: false });
222
+ const ul = container.querySelector('ul');
223
+ // An empty branch materializes its anchor at once, so it has a position.
224
+ expect(text_nodes(ul)).toBe(1);
225
+ expect(ul.firstChild.nodeType).toBe(3);
226
+ expect(ul.children.length).toBe(1);
227
+
228
+ container.querySelector('.show').click();
229
+ flushSync();
230
+ expect(Array.from(ul.children).map((li) => li.textContent)).toEqual(['1', '2']);
231
+ // The content renders before its anchor.
232
+ expect(ul.children[0].nextSibling.nodeType).toBe(3);
233
+
234
+ container.querySelector('.reverse').click();
235
+ flushSync();
236
+ expect(Array.from(ul.children).map((li) => li.textContent)).toEqual(['2', '1']);
237
+ // The anchor travelled with item 1 to the end.
238
+ expect(ul.lastChild.nodeType).toBe(3);
239
+ expect(ul.lastChild.previousSibling).toBe(ul.children[1]);
240
+
241
+ unmount();
242
+ expect(container.childNodes.length).toBe(0);
243
+ });
244
+ });
130
245
  });