ripple 0.3.127 → 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 +293 -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 +192 -114
  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 +14 -5
  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 +185 -124
  14. package/src/runtime/internal/client/for.js +616 -264
  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 +9 -13
  20. package/src/runtime/internal/client/operations.js +44 -51
  21. package/src/runtime/internal/client/portal.js +163 -85
  22. package/src/runtime/internal/client/render.js +44 -25
  23. package/src/runtime/internal/client/rpc.js +3 -3
  24. package/src/runtime/internal/client/runtime.js +799 -576
  25. package/src/runtime/internal/client/selector.js +77 -0
  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 +65 -21
  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 +327 -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 +97 -29
  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 +221 -0
  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 +190 -150
  103. package/tests/hydration/compiled/client/for.js +1472 -1126
  104. package/tests/hydration/compiled/client/fragment-cursor.js +2341 -0
  105. package/tests/hydration/compiled/client/head.js +255 -199
  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 -400
  113. package/tests/hydration/compiled/client/nested-control-flow.js +1326 -1255
  114. package/tests/hydration/compiled/client/portal.js +87 -95
  115. package/tests/hydration/compiled/client/reactivity.js +140 -83
  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 +19 -19
  126. package/tests/hydration/compiled/server/for.js +109 -49
  127. package/tests/hydration/compiled/server/fragment-cursor.js +1687 -0
  128. package/tests/hydration/compiled/server/head.js +20 -28
  129. package/tests/hydration/compiled/server/hmr.js +2 -2
  130. package/tests/hydration/compiled/server/html.js +71 -247
  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 -46
  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
@@ -3,79 +3,79 @@ import { track } from 'ripple';
3
3
  // Event handling components for hydration testing
4
4
 
5
5
  export function ClickCounter() @{
6
- let &[count] = track(0);
6
+ const count = track(0);
7
7
  <div>
8
8
  <button
9
9
  class="increment"
10
10
  onClick={() => {
11
- count++;
11
+ count.value++;
12
12
  }}
13
13
  >{'Increment'}</button>
14
- <span class="count">{count}</span>
14
+ <span class="count">{count.value}</span>
15
15
  </div>
16
16
  }
17
17
 
18
18
  export function IncrementDecrement() @{
19
- let &[count] = track(0);
19
+ const count = track(0);
20
20
  <div>
21
21
  <button
22
22
  class="decrement"
23
23
  onClick={() => {
24
- count--;
24
+ count.value--;
25
25
  }}
26
26
  >{'-'}</button>
27
- <span class="count">{count}</span>
27
+ <span class="count">{count.value}</span>
28
28
  <button
29
29
  class="increment"
30
30
  onClick={() => {
31
- count++;
31
+ count.value++;
32
32
  }}
33
33
  >{'+'}</button>
34
34
  </div>
35
35
  }
36
36
 
37
37
  export function MultipleEvents() @{
38
- let &[clicks] = track(0);
39
- let &[hovers] = track(0);
38
+ const clicks = track(0);
39
+ const hovers = track(0);
40
40
  <div>
41
41
  <button
42
42
  class="target"
43
43
  onClick={() => {
44
- clicks++;
44
+ clicks.value++;
45
45
  }}
46
46
  onMouseEnter={() => {
47
- hovers++;
47
+ hovers.value++;
48
48
  }}
49
49
  >{'Target'}</button>
50
- <span class="clicks">{clicks}</span>
51
- <span class="hovers">{hovers}</span>
50
+ <span class="clicks">{clicks.value}</span>
51
+ <span class="hovers">{hovers.value}</span>
52
52
  </div>
53
53
  }
54
54
 
55
55
  export function MultiStateUpdate() @{
56
- let &[count] = track(0);
57
- let &[lastAction] = track('none');
56
+ const count = track(0);
57
+ const lastAction = track('none');
58
58
  const handleClick = () => {
59
- count++;
60
- lastAction = 'increment';
59
+ count.value++;
60
+ lastAction.value = 'increment';
61
61
  };
62
62
  <div>
63
63
  <button class="btn" onClick={handleClick}>{'Click'}</button>
64
- <span class="count">{count}</span>
65
- <span class="action">{lastAction}</span>
64
+ <span class="count">{count.value}</span>
65
+ <span class="action">{lastAction.value}</span>
66
66
  </div>
67
67
  }
68
68
 
69
69
  export function ToggleButton() @{
70
- let &[isOn] = track(false);
70
+ const isOn = track(false);
71
71
  <div>
72
72
  <button
73
73
  class="toggle"
74
74
  onClick={() => {
75
- isOn = !isOn;
75
+ isOn.value = !isOn.value;
76
76
  }}
77
77
  >
78
- {isOn ? 'ON' : 'OFF'}
78
+ {isOn.value ? 'ON' : 'OFF'}
79
79
  </button>
80
80
  </div>
81
81
  }
@@ -85,14 +85,14 @@ export function ChildButton(props: { onClick: () => void; label: string }) @{
85
85
  }
86
86
 
87
87
  export function ParentWithChildButton() @{
88
- let &[count] = track(0);
88
+ const count = track(0);
89
89
  <div>
90
90
  <ChildButton
91
91
  onClick={() => {
92
- count++;
92
+ count.value++;
93
93
  }}
94
94
  label="Click me"
95
95
  />
96
- <span class="count">{count}</span>
96
+ <span class="count">{count.value}</span>
97
97
  </div>
98
98
  }
@@ -1,4 +1,4 @@
1
- import { track } from 'ripple';
1
+ import { track, type Tracked } from 'ripple';
2
2
 
3
3
  // For loop components for hydration testing
4
4
 
@@ -34,16 +34,16 @@ export function KeyedForLoop() @{
34
34
  }
35
35
 
36
36
  export function ReactiveForLoopAdd() @{
37
- let &[items] = track(['A', 'B']);
37
+ const items = track(['A', 'B']);
38
38
  <>
39
39
  <button
40
40
  class="add"
41
41
  onClick={() => {
42
- items = [...items, 'C'];
42
+ items.value = [...items.value, 'C'];
43
43
  }}
44
44
  >{'Add'}</button>
45
45
  <ul>
46
- @for (const item of items) {
46
+ @for (const item of items.value) {
47
47
  <li>{item}</li>
48
48
  }
49
49
  </ul>
@@ -51,16 +51,16 @@ export function ReactiveForLoopAdd() @{
51
51
  }
52
52
 
53
53
  export function ReactiveForLoopRemove() @{
54
- let &[items] = track(['A', 'B', 'C']);
54
+ const items = track(['A', 'B', 'C']);
55
55
  <>
56
56
  <button
57
57
  class="remove"
58
58
  onClick={() => {
59
- items = items.slice(0, -1);
59
+ items.value = items.value.slice(0, -1);
60
60
  }}
61
61
  >{'Remove'}</button>
62
62
  <ul>
63
- @for (const item of items) {
63
+ @for (const item of items.value) {
64
64
  <li>{item}</li>
65
65
  }
66
66
  </ul>
@@ -68,17 +68,17 @@ export function ReactiveForLoopRemove() @{
68
68
  }
69
69
 
70
70
  export function ForLoopInteractive() @{
71
- let &[counts] = track([0, 0, 0]);
71
+ const counts = track([0, 0, 0]);
72
72
  <div>
73
- @for (const count of counts; index i) {
73
+ @for (const count of counts.value; index i) {
74
74
  <div class={`item-${i}`}>
75
75
  <span class="value">{count}</span>
76
76
  <button
77
77
  class="increment"
78
78
  onClick={() => {
79
- const newCounts = [...counts];
79
+ const newCounts = [...counts.value];
80
80
  newCounts[i]++;
81
- counts = newCounts;
81
+ counts.value = newCounts;
82
82
  }}
83
83
  >{'+'}</button>
84
84
  </div>
@@ -128,7 +128,7 @@ export function ForLoopComplexObjects() @{
128
128
 
129
129
  // Test reordering items in a keyed for loop
130
130
  export function KeyedForLoopReorder() @{
131
- let &[items] = track([
131
+ const items = track([
132
132
  { id: 1, name: 'First' },
133
133
  { id: 2, name: 'Second' },
134
134
  { id: 3, name: 'Third' },
@@ -137,11 +137,11 @@ export function KeyedForLoopReorder() @{
137
137
  <button
138
138
  class="reorder"
139
139
  onClick={() => {
140
- items = [items[2], items[0], items[1]];
140
+ items.value = [items.value[2], items.value[0], items.value[1]];
141
141
  }}
142
142
  >{'Reorder'}</button>
143
143
  <ul>
144
- @for (const item of items; key item.id) {
144
+ @for (const item of items.value; key item.id) {
145
145
  <li class={`item-${item.id}`}>{item.name}</li>
146
146
  }
147
147
  </ul>
@@ -150,7 +150,7 @@ export function KeyedForLoopReorder() @{
150
150
 
151
151
  // Test for loop with item property updates (keyed)
152
152
  export function KeyedForLoopUpdate() @{
153
- let &[items] = track([
153
+ const items = track([
154
154
  { id: 1, name: 'Item 1' },
155
155
  { id: 2, name: 'Item 2' },
156
156
  ]);
@@ -158,11 +158,13 @@ export function KeyedForLoopUpdate() @{
158
158
  <button
159
159
  class="update"
160
160
  onClick={() => {
161
- items = items.map((item) => (item.id === 1 ? { ...item, name: 'Updated' } : item));
161
+ items.value = items.value.map(
162
+ (item) => (item.id === 1 ? { ...item, name: 'Updated' } : item),
163
+ );
162
164
  }}
163
165
  >{'Update'}</button>
164
166
  <ul>
165
- @for (const item of items; key item.id) {
167
+ @for (const item of items.value; key item.id) {
166
168
  <li class={`item-${item.id}`}>{item.name}</li>
167
169
  }
168
170
  </ul>
@@ -171,17 +173,17 @@ export function KeyedForLoopUpdate() @{
171
173
 
172
174
  // Test for loop with combined add/remove/reorder
173
175
  export function ForLoopMixedOperations() @{
174
- let &[items] = track(['A', 'B', 'C', 'D']);
176
+ const items = track(['A', 'B', 'C', 'D']);
175
177
  <>
176
178
  <button
177
179
  class="shuffle"
178
180
  onClick={() => {
179
181
  // Remove B, add E, reorder to D, C, A, E
180
- items = ['D', 'C', 'A', 'E'];
182
+ items.value = ['D', 'C', 'A', 'E'];
181
183
  }}
182
184
  >{'Shuffle'}</button>
183
185
  <ul>
184
- @for (const item of items) {
186
+ @for (const item of items.value) {
185
187
  <li class={`item-${item}`}>{item}</li>
186
188
  }
187
189
  </ul>
@@ -190,24 +192,24 @@ export function ForLoopMixedOperations() @{
190
192
 
191
193
  // Test for loop inside if block (combined control flow)
192
194
  export function ForLoopInsideIf() @{
193
- let &[showList] = track(true);
194
- let &[items] = track(['X', 'Y', 'Z']);
195
+ const showList = track(true);
196
+ const items = track(['X', 'Y', 'Z']);
195
197
  <>
196
198
  <button
197
199
  class="toggle"
198
200
  onClick={() => {
199
- showList = !showList;
201
+ showList.value = !showList.value;
200
202
  }}
201
203
  >{'Toggle List'}</button>
202
204
  <button
203
205
  class="add"
204
206
  onClick={() => {
205
- items = [...items, 'W'];
207
+ items.value = [...items.value, 'W'];
206
208
  }}
207
209
  >{'Add Item'}</button>
208
- @if (showList) {
210
+ @if (showList.value) {
209
211
  <ul class="list">
210
- @for (const item of items) {
212
+ @for (const item of items.value) {
211
213
  <li>{item}</li>
212
214
  }
213
215
  </ul>
@@ -217,16 +219,16 @@ export function ForLoopInsideIf() @{
217
219
 
218
220
  // Test for loop that transitions from empty to populated
219
221
  export function ForLoopEmptyToPopulated() @{
220
- let &[items] = track<string[]>([]);
222
+ const items = track<string[]>([]);
221
223
  <>
222
224
  <button
223
225
  class="populate"
224
226
  onClick={() => {
225
- items = ['One', 'Two', 'Three'];
227
+ items.value = ['One', 'Two', 'Three'];
226
228
  }}
227
229
  >{'Populate'}</button>
228
230
  <ul class="list">
229
- @for (const item of items) {
231
+ @for (const item of items.value) {
230
232
  <li>{item}</li>
231
233
  }
232
234
  </ul>
@@ -235,16 +237,16 @@ export function ForLoopEmptyToPopulated() @{
235
237
 
236
238
  // Test for loop that transitions from populated to empty
237
239
  export function ForLoopPopulatedToEmpty() @{
238
- let &[items] = track(['One', 'Two', 'Three']);
240
+ const items = track(['One', 'Two', 'Three']);
239
241
  <>
240
242
  <button
241
243
  class="clear"
242
244
  onClick={() => {
243
- items = [];
245
+ items.value = [];
244
246
  }}
245
247
  >{'Clear'}</button>
246
248
  <ul class="list">
247
- @for (const item of items) {
249
+ @for (const item of items.value) {
248
250
  <li>{item}</li>
249
251
  }
250
252
  </ul>
@@ -253,7 +255,7 @@ export function ForLoopPopulatedToEmpty() @{
253
255
 
254
256
  // Test nested for loops with reactivity
255
257
  export function NestedForLoopReactive() @{
256
- let &[grid] = track([
258
+ const grid = track([
257
259
  [1, 2],
258
260
  [3, 4],
259
261
  ]);
@@ -261,19 +263,19 @@ export function NestedForLoopReactive() @{
261
263
  <button
262
264
  class="add-row"
263
265
  onClick={() => {
264
- grid = [...grid, [5, 6]];
266
+ grid.value = [...grid.value, [5, 6]];
265
267
  }}
266
268
  >{'Add Row'}</button>
267
269
  <button
268
270
  class="update-cell"
269
271
  onClick={() => {
270
- const newGrid = grid.map((row) => [...row]);
272
+ const newGrid = grid.value.map((row) => [...row]);
271
273
  newGrid[0][0] = 99;
272
- grid = newGrid;
274
+ grid.value = newGrid;
273
275
  }}
274
276
  >{'Update Cell'}</button>
275
277
  <div class="grid">
276
- @for (const row of grid; index rowIndex) {
278
+ @for (const row of grid.value; index rowIndex) {
277
279
  <div class={`row-${rowIndex}`}>
278
280
  @for (const cell of row; index colIndex) {
279
281
  <span class={`cell-${rowIndex}-${colIndex}`}>{cell}</span>
@@ -324,16 +326,16 @@ export function ForLoopDeeplyNested() @{
324
326
 
325
327
  // Test for loop with index that gets updated
326
328
  export function ForLoopIndexUpdate() @{
327
- let &[items] = track(['First', 'Second', 'Third']);
329
+ const items = track(['First', 'Second', 'Third']);
328
330
  <>
329
331
  <button
330
332
  class="prepend"
331
333
  onClick={() => {
332
- items = ['Zeroth', ...items];
334
+ items.value = ['Zeroth', ...items.value];
333
335
  }}
334
336
  >{'Prepend'}</button>
335
337
  <ul>
336
- @for (const item of items; index i) {
338
+ @for (const item of items.value; index i) {
337
339
  <li class={`item-${i}`}>{`[${i}] ${item}`}</li>
338
340
  }
339
341
  </ul>
@@ -342,7 +344,7 @@ export function ForLoopIndexUpdate() @{
342
344
 
343
345
  // Test keyed for loop with index
344
346
  export function KeyedForLoopWithIndex() @{
345
- let &[items] = track([
347
+ const items = track([
346
348
  { id: 'a', value: 'Alpha' },
347
349
  { id: 'b', value: 'Beta' },
348
350
  { id: 'c', value: 'Gamma' },
@@ -351,11 +353,11 @@ export function KeyedForLoopWithIndex() @{
351
353
  <button
352
354
  class="reorder"
353
355
  onClick={() => {
354
- items = [items[1], items[2], items[0]];
356
+ items.value = [items.value[1], items.value[2], items.value[0]];
355
357
  }}
356
358
  >{'Rotate'}</button>
357
359
  <ul>
358
- @for (const item of items; index i; key item.id) {
360
+ @for (const item of items.value; index i; key item.id) {
359
361
  <li class={`item-${item.id}`} data-index={i}>{`[${i}] ${item.id}: ${item.value}`}</li>
360
362
  }
361
363
  </ul>
@@ -364,11 +366,11 @@ export function KeyedForLoopWithIndex() @{
364
366
 
365
367
  // Test for loop with sibling elements
366
368
  export function ForLoopWithSiblings() @{
367
- let &[items] = track(['A', 'B']);
369
+ const items = track(['A', 'B']);
368
370
  <>
369
371
  <div class="wrapper">
370
372
  <header class="before">{'Before'}</header>
371
- @for (const item of items) {
373
+ @for (const item of items.value) {
372
374
  <div class={`item-${item}`}>{item}</div>
373
375
  }
374
376
  <footer class="after">{'After'}</footer>
@@ -376,7 +378,7 @@ export function ForLoopWithSiblings() @{
376
378
  <button
377
379
  class="add"
378
380
  onClick={() => {
379
- items = [...items, 'C'];
381
+ items.value = [...items.value, 'C'];
380
382
  }}
381
383
  >{'Add'}</button>
382
384
  </>
@@ -397,17 +399,17 @@ export function ForLoopItemState() @{
397
399
  }
398
400
 
399
401
  function TodoItem(props: { id: number; text: string }) @{
400
- let &[done] = track(false);
402
+ const done = track(false);
401
403
  <div class={`todo-${props.id}`}>
402
404
  <input
403
405
  type="checkbox"
404
406
  class="checkbox"
405
- checked={done}
407
+ checked={done.value}
406
408
  onChange={(e) => {
407
- done = (e.target as HTMLInputElement).checked;
409
+ done.value = (e.target as HTMLInputElement).checked;
408
410
  }}
409
411
  />
410
- <span class={done ? 'completed' : 'pending'}>{props.text}</span>
412
+ <span class={done.value ? 'completed' : 'pending'}>{props.text}</span>
411
413
  </div>
412
414
  }
413
415
 
@@ -423,16 +425,16 @@ export function ForLoopSingleItem() @{
423
425
 
424
426
  // Test for loop adding at beginning
425
427
  export function ForLoopAddAtBeginning() @{
426
- let &[items] = track(['B', 'C']);
428
+ const items = track(['B', 'C']);
427
429
  <>
428
430
  <button
429
431
  class="prepend"
430
432
  onClick={() => {
431
- items = ['A', ...items];
433
+ items.value = ['A', ...items.value];
432
434
  }}
433
435
  >{'Prepend A'}</button>
434
436
  <ul>
435
- @for (const item of items) {
437
+ @for (const item of items.value) {
436
438
  <li class={`item-${item}`}>{item}</li>
437
439
  }
438
440
  </ul>
@@ -441,18 +443,18 @@ export function ForLoopAddAtBeginning() @{
441
443
 
442
444
  // Test for loop adding in the middle
443
445
  export function ForLoopAddInMiddle() @{
444
- let &[items] = track(['A', 'C']);
446
+ const items = track(['A', 'C']);
445
447
  <>
446
448
  <button
447
449
  class="insert"
448
450
  onClick={() => {
449
- const copy = [...items];
451
+ const copy = [...items.value];
450
452
  copy.splice(1, 0, 'B');
451
- items = copy;
453
+ items.value = copy;
452
454
  }}
453
455
  >{'Insert B'}</button>
454
456
  <ul>
455
- @for (const item of items) {
457
+ @for (const item of items.value) {
456
458
  <li class={`item-${item}`}>{item}</li>
457
459
  }
458
460
  </ul>
@@ -461,16 +463,16 @@ export function ForLoopAddInMiddle() @{
461
463
 
462
464
  // Test for loop removing from the middle
463
465
  export function ForLoopRemoveFromMiddle() @{
464
- let &[items] = track(['A', 'B', 'C']);
466
+ const items = track(['A', 'B', 'C']);
465
467
  <>
466
468
  <button
467
469
  class="remove-middle"
468
470
  onClick={() => {
469
- items = items.filter((item) => item !== 'B');
471
+ items.value = items.value.filter((item) => item !== 'B');
470
472
  }}
471
473
  >{'Remove B'}</button>
472
474
  <ul>
473
- @for (const item of items) {
475
+ @for (const item of items.value) {
474
476
  <li class={`item-${item}`}>{item}</li>
475
477
  }
476
478
  </ul>
@@ -489,18 +491,18 @@ export function ForLoopLargeList() @{
489
491
 
490
492
  // Test for loop with swap operation
491
493
  export function ForLoopSwap() @{
492
- let &[items] = track(['A', 'B', 'C', 'D']);
494
+ const items = track(['A', 'B', 'C', 'D']);
493
495
  <>
494
496
  <button
495
497
  class="swap"
496
498
  onClick={() => {
497
- const copy = [...items];
499
+ const copy = [...items.value];
498
500
  [copy[0], copy[3]] = [copy[3], copy[0]];
499
- items = copy;
501
+ items.value = copy;
500
502
  }}
501
503
  >{'Swap First and Last'}</button>
502
504
  <ul>
503
- @for (const item of items) {
505
+ @for (const item of items.value) {
504
506
  <li class={`item-${item}`}>{item}</li>
505
507
  }
506
508
  </ul>
@@ -509,18 +511,64 @@ export function ForLoopSwap() @{
509
511
 
510
512
  // Test for loop with reverse operation
511
513
  export function ForLoopReverse() @{
512
- let &[items] = track(['A', 'B', 'C', 'D']);
514
+ const items = track(['A', 'B', 'C', 'D']);
513
515
  <>
514
516
  <button
515
517
  class="reverse"
516
518
  onClick={() => {
517
- items = [...items].reverse();
519
+ items.value = [...items.value].reverse();
518
520
  }}
519
521
  >{'Reverse'}</button>
520
522
  <ul>
521
- @for (const item of items) {
523
+ @for (const item of items.value) {
522
524
  <li class={`item-${item}`}>{item}</li>
523
525
  }
524
526
  </ul>
525
527
  </>
526
528
  }
529
+
530
+ // Inserting at or moving to the end of a hydrated keyed list must land after
531
+ // the hydrated items, before the following sibling.
532
+ export function KeyedForLoopAppendAndRotate() @{
533
+ const items = track([1, 2, 3]);
534
+ <div class="wrapper">
535
+ <div class="host">
536
+ @for (const item of items.value; key item) {
537
+ <span class="item">{item}</span>
538
+ }
539
+ <p class="tail">{'p'}</p>
540
+ </div>
541
+ <button
542
+ class="push"
543
+ onClick={() => (items.value = [...items.value, items.value.length + 1])}
544
+ >{'Push'}</button>
545
+ <button
546
+ class="rotate"
547
+ onClick={() => (items.value = [...items.value.slice(1), items.value[0]])}
548
+ >{'Rotate'}</button>
549
+ </div>
550
+ }
551
+
552
+ function RootKeyedList(props: { items: Tracked<number[]> }) @{
553
+ @for (const item of props.items.value; key item) {
554
+ <span class="item">{item}</span>
555
+ }
556
+ }
557
+
558
+ export function RootKeyedForLoopAppendAndRotate() @{
559
+ const items = track([1, 2, 3]);
560
+ <div class="wrapper">
561
+ <div class="host">
562
+ <RootKeyedList {items} />
563
+ <p class="tail">{'p'}</p>
564
+ </div>
565
+ <button
566
+ class="push"
567
+ onClick={() => (items.value = [...items.value, items.value.length + 1])}
568
+ >{'Push'}</button>
569
+ <button
570
+ class="rotate"
571
+ onClick={() => (items.value = [...items.value.slice(1), items.value[0]])}
572
+ >{'Rotate'}</button>
573
+ </div>
574
+ }