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.
- package/CHANGELOG.md +293 -0
- package/package.json +10 -4
- package/src/runtime/element.js +23 -6
- package/src/runtime/index-client.js +81 -41
- package/src/runtime/index-server.js +1 -0
- package/src/runtime/internal/client/blocks.js +192 -114
- package/src/runtime/internal/client/component.js +4 -30
- package/src/runtime/internal/client/composite.js +12 -13
- package/src/runtime/internal/client/constants.js +14 -5
- package/src/runtime/internal/client/context.js +12 -19
- package/src/runtime/internal/client/css.js +7 -9
- package/src/runtime/internal/client/events.js +178 -90
- package/src/runtime/internal/client/expression.js +185 -124
- package/src/runtime/internal/client/for.js +616 -264
- package/src/runtime/internal/client/head.js +10 -5
- package/src/runtime/internal/client/html.js +1 -0
- package/src/runtime/internal/client/hydration.js +152 -10
- package/src/runtime/internal/client/if.js +239 -50
- package/src/runtime/internal/client/index.js +9 -13
- package/src/runtime/internal/client/operations.js +44 -51
- package/src/runtime/internal/client/portal.js +163 -85
- package/src/runtime/internal/client/render.js +44 -25
- package/src/runtime/internal/client/rpc.js +3 -3
- package/src/runtime/internal/client/runtime.js +799 -576
- package/src/runtime/internal/client/selector.js +77 -0
- package/src/runtime/internal/client/template.js +133 -158
- package/src/runtime/internal/client/transport.js +38 -0
- package/src/runtime/internal/client/try.js +465 -426
- package/src/runtime/internal/client/types.d.ts +65 -21
- package/src/runtime/internal/client/utils.js +1 -3
- package/src/runtime/internal/server/dynamic.js +20 -21
- package/src/runtime/internal/server/index.js +327 -275
- package/src/runtime/internal/server/rpc.js +3 -2
- package/src/runtime/internal/server/transport.js +16 -0
- package/src/runtime/transport.js +21 -0
- package/tests/client/__snapshots__/for.test.tsrx.snap +0 -5
- package/tests/client/array/array.copy-within.test.tsrx +12 -12
- package/tests/client/array/array.derived.test.tsrx +46 -46
- package/tests/client/array/array.iteration.test.tsrx +10 -10
- package/tests/client/array/array.mutations.test.tsrx +20 -20
- package/tests/client/array/array.static.test.tsrx +11 -11
- package/tests/client/array/array.to-methods.test.tsrx +6 -6
- package/tests/client/async-suspend.test.tsrx +74 -75
- package/tests/client/basic/basic.attributes.test.tsrx +86 -83
- package/tests/client/basic/basic.collections.test.tsrx +12 -12
- package/tests/client/basic/basic.components.test.tsrx +99 -25
- package/tests/client/basic/basic.errors.test.tsrx +18 -18
- package/tests/client/basic/basic.events.test.tsrx +42 -42
- package/tests/client/basic/basic.get-set.test.tsrx +8 -8
- package/tests/client/basic/basic.reactivity.test.tsrx +97 -29
- package/tests/client/basic/basic.rendering.test.tsrx +25 -25
- package/tests/client/basic/basic.utilities.test.tsrx +3 -3
- package/tests/client/boundaries.test.tsrx +8 -8
- package/tests/client/boxed-locals.test.tsrx +452 -0
- package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +6 -6
- package/tests/client/compiler/compiler.assignments.test.tsrx +9 -73
- package/tests/client/compiler/compiler.attributes.test.tsrx +23 -23
- package/tests/client/compiler/compiler.basic.test.tsrx +15 -60
- package/tests/client/compiler/compiler.tracked-access.test.tsrx +18 -43
- package/tests/client/compiler/compiler.try-in-function.test.tsrx +2 -2
- package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -1
- package/tests/client/composite/composite.dynamic-components.test.tsrx +77 -6
- package/tests/client/composite/composite.props.test.tsrx +149 -12
- package/tests/client/composite/composite.reactivity.test.tsrx +51 -52
- package/tests/client/composite/composite.render.test.tsrx +26 -9
- package/tests/client/css/scoped-styles.test.tsrx +8 -8
- package/tests/client/date.test.tsrx +42 -42
- package/tests/client/derived-release.test.tsrx +370 -0
- package/tests/client/dynamic-elements.test.tsrx +45 -45
- package/tests/client/events.test.tsrx +97 -71
- package/tests/client/for-selector.test.tsrx +221 -0
- package/tests/client/for-single-root-items.test.tsrx +118 -3
- package/tests/client/for.test.tsrx +332 -31
- package/tests/client/head.test.tsrx +19 -19
- package/tests/client/html.test.tsrx +11 -9
- package/tests/client/if-block.test.tsrx +80 -0
- package/tests/client/map.test.tsrx +16 -16
- package/tests/client/media-query.test.tsrx +7 -7
- package/tests/client/portal.test.tsrx +224 -26
- package/tests/client/primitive-text.test.tsrx +50 -0
- package/tests/client/prop-kind-switch.test.tsrx +141 -0
- package/tests/client/ref.test.tsrx +17 -17
- package/tests/client/return.test.tsrx +26 -3
- package/tests/client/root-boundary.test.tsrx +42 -0
- package/tests/client/scoped-flush.test.tsrx +8 -8
- package/tests/client/set.test.tsrx +6 -6
- package/tests/client/svg.test.tsrx +11 -11
- package/tests/client/switch.test.tsrx +44 -44
- package/tests/client/track-async-hydration.test.tsrx +4 -4
- package/tests/client/try.test.tsrx +151 -83
- package/tests/client/tsx.test.tsrx +43 -40
- package/tests/client/url/url.derived.test.tsrx +6 -6
- package/tests/client/url-search-params/url-search-params.derived.test.tsrx +8 -8
- package/tests/client/url-search-params/url-search-params.iteration.test.tsrx +10 -10
- package/tests/client/url-search-params/url-search-params.mutation.test.tsrx +10 -10
- package/tests/client/url-search-params/url-search-params.retrieval.test.tsrx +18 -18
- package/tests/client/url-search-params/url-search-params.serialization.test.tsrx +2 -2
- package/tests/hydration/basic.test.js +74 -1
- package/tests/hydration/build-components.js +22 -1
- package/tests/hydration/compiled/client/basic.js +1142 -713
- package/tests/hydration/compiled/client/composite.js +151 -100
- package/tests/hydration/compiled/client/events.js +190 -150
- package/tests/hydration/compiled/client/for.js +1472 -1126
- package/tests/hydration/compiled/client/fragment-cursor.js +2341 -0
- package/tests/hydration/compiled/client/head.js +255 -199
- package/tests/hydration/compiled/client/hmr.js +54 -41
- package/tests/hydration/compiled/client/html-in-template.js +54 -39
- package/tests/hydration/compiled/client/html.js +1633 -1263
- package/tests/hydration/compiled/client/if-children.js +420 -270
- package/tests/hydration/compiled/client/if-fragment-controlflow.js +363 -317
- package/tests/hydration/compiled/client/if.js +229 -219
- package/tests/hydration/compiled/client/mixed-control-flow.js +420 -400
- package/tests/hydration/compiled/client/nested-control-flow.js +1326 -1255
- package/tests/hydration/compiled/client/portal.js +87 -95
- package/tests/hydration/compiled/client/reactivity.js +140 -83
- package/tests/hydration/compiled/client/return.js +28 -19
- package/tests/hydration/compiled/client/streaming.js +390 -306
- package/tests/hydration/compiled/client/switch.js +258 -266
- package/tests/hydration/compiled/client/track-async-serialization.js +448 -280
- package/tests/hydration/compiled/client/try.js +173 -129
- package/tests/hydration/compiled/client/typed-text.js +71 -0
- package/tests/hydration/compiled/fixtures/money.js +10 -0
- package/tests/hydration/compiled/server/basic.js +123 -6
- package/tests/hydration/compiled/server/composite.js +16 -30
- package/tests/hydration/compiled/server/events.js +19 -19
- package/tests/hydration/compiled/server/for.js +109 -49
- package/tests/hydration/compiled/server/fragment-cursor.js +1687 -0
- package/tests/hydration/compiled/server/head.js +20 -28
- package/tests/hydration/compiled/server/hmr.js +2 -2
- package/tests/hydration/compiled/server/html.js +71 -247
- package/tests/hydration/compiled/server/if-children.js +107 -15
- package/tests/hydration/compiled/server/if.js +11 -15
- package/tests/hydration/compiled/server/mixed-control-flow.js +8 -8
- package/tests/hydration/compiled/server/portal.js +14 -22
- package/tests/hydration/compiled/server/reactivity.js +14 -46
- package/tests/hydration/compiled/server/streaming.js +19 -19
- package/tests/hydration/compiled/server/switch.js +8 -8
- package/tests/hydration/compiled/server/track-async-serialization.js +150 -23
- package/tests/hydration/compiled/server/try.js +8 -8
- package/tests/hydration/compiled/server/typed-text.js +17 -0
- package/tests/hydration/components/basic.tsrx +44 -8
- package/tests/hydration/components/composite.tsrx +2 -2
- package/tests/hydration/components/events.tsrx +25 -25
- package/tests/hydration/components/for.tsrx +115 -67
- package/tests/hydration/components/fragment-cursor.tsrx +650 -0
- package/tests/hydration/components/head.tsrx +16 -16
- package/tests/hydration/components/hmr.tsrx +2 -2
- package/tests/hydration/components/html.tsrx +7 -7
- package/tests/hydration/components/if-children.tsrx +72 -25
- package/tests/hydration/components/if.tsrx +18 -18
- package/tests/hydration/components/mixed-control-flow.tsrx +11 -11
- package/tests/hydration/components/portal.tsrx +3 -3
- package/tests/hydration/components/reactivity.tsrx +16 -16
- package/tests/hydration/components/streaming.tsrx +20 -20
- package/tests/hydration/components/switch.tsrx +20 -20
- package/tests/hydration/components/text-types.ts +4 -0
- package/tests/hydration/components/track-async-serialization.tsrx +80 -26
- package/tests/hydration/components/try.tsrx +8 -8
- package/tests/hydration/components/typed-text.tsrx +22 -0
- package/tests/hydration/fixtures/money.js +10 -0
- package/tests/hydration/for.test.js +36 -0
- package/tests/hydration/fragment-cursor.test.js +111 -0
- package/tests/hydration/if-children.test.js +24 -0
- package/tests/hydration/streaming.test.js +19 -0
- package/tests/hydration/track-async-serialization.test.js +87 -2
- package/tests/hydration/tsconfig.text-types.json +10 -0
- package/tests/hydration/typed-text.test.js +34 -0
- package/tests/server/await.test.tsrx +3 -3
- package/tests/server/basic.attributes.test.tsrx +42 -42
- package/tests/server/basic.components.test.tsrx +64 -8
- package/tests/server/basic.test.tsrx +34 -32
- package/tests/server/compiler.test.tsrx +1 -70
- package/tests/server/composite.props.test.tsrx +6 -6
- package/tests/server/dynamic-elements.test.tsrx +31 -8
- package/tests/server/for.test.tsrx +94 -0
- package/tests/server/head.test.tsrx +11 -11
- package/tests/server/scoped-styles.test.tsrx +2 -2
- package/tests/server/streaming-ssr.test.tsrx +48 -43
- package/tests/server/track-async-serialization.test.tsrx +204 -21
- package/tests/server/try.test.tsrx +48 -48
- package/tests/types/transport.ts +40 -0
- package/tests/utils/vite-plugin-config.test.js +23 -0
- package/types/index.d.ts +48 -7
- package/types/server.d.ts +3 -0
- package/types/transport.d.ts +15 -0
- package/src/runtime/internal/client/switch.js +0 -113
- package/src/utils/errors.js +0 -13
- package/tests/client/lazy-array.test.tsrx +0 -31
- package/tests/client/lazy-destructuring.test.tsrx +0 -438
- package/tests/client/tracked-index-access.test.tsrx +0 -113
- package/tests/server/lazy-destructuring.test.tsrx +0 -595
- package/tests/server/tracked-index-access.test.tsrx +0 -76
|
@@ -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
|
-
|
|
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
|
});
|
|
@@ -166,18 +166,18 @@ describe('for statements', () => {
|
|
|
166
166
|
|
|
167
167
|
it('correctly handles keyed for...of loops', () => {
|
|
168
168
|
function App() @{
|
|
169
|
-
|
|
169
|
+
const items = track([
|
|
170
170
|
{ id: 1, text: 'Item 1' },
|
|
171
171
|
{ id: 2, text: 'Item 2' },
|
|
172
172
|
{ id: 3, text: 'Item 3' },
|
|
173
173
|
]);
|
|
174
174
|
<>
|
|
175
|
-
@for (let item of items; index i; key item.id) {
|
|
175
|
+
@for (let item of items.value; index i; key item.id) {
|
|
176
176
|
<div>{i + ':' + item.text}</div>
|
|
177
177
|
}
|
|
178
178
|
<button
|
|
179
179
|
onClick={() => {
|
|
180
|
-
items = items.toReversed();
|
|
180
|
+
items.value = items.value.toReversed();
|
|
181
181
|
}}
|
|
182
182
|
>{'Reverse'}</button>
|
|
183
183
|
</>
|
|
@@ -195,22 +195,63 @@ describe('for statements', () => {
|
|
|
195
195
|
expect(container).toMatchSnapshot();
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
it('keyed by identity re-reads a plain array mutated in place', () => {
|
|
199
|
+
const list = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
|
200
|
+
let bump = () => {};
|
|
201
|
+
|
|
202
|
+
function App() @{
|
|
203
|
+
const version = track(0);
|
|
204
|
+
bump = () => {
|
|
205
|
+
version.value = version.value + 1;
|
|
206
|
+
};
|
|
207
|
+
<ul>
|
|
208
|
+
@for (const item of (version.value, list); key item) {
|
|
209
|
+
<li>{item.id}</li>
|
|
210
|
+
}
|
|
211
|
+
</ul>
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
render(App);
|
|
215
|
+
const texts = () => Array.from(container.querySelectorAll('li'), (el) => el.textContent);
|
|
216
|
+
expect(texts()).toEqual(['1', '2', '3']);
|
|
217
|
+
|
|
218
|
+
list.push({ id: 4 });
|
|
219
|
+
bump();
|
|
220
|
+
flushSync();
|
|
221
|
+
expect(texts()).toEqual(['1', '2', '3', '4']);
|
|
222
|
+
|
|
223
|
+
list.splice(0, 1);
|
|
224
|
+
bump();
|
|
225
|
+
flushSync();
|
|
226
|
+
expect(texts()).toEqual(['2', '3', '4']);
|
|
227
|
+
|
|
228
|
+
list.reverse();
|
|
229
|
+
bump();
|
|
230
|
+
flushSync();
|
|
231
|
+
expect(texts()).toEqual(['4', '3', '2']);
|
|
232
|
+
|
|
233
|
+
list.splice(1, 1, { id: 5 });
|
|
234
|
+
bump();
|
|
235
|
+
flushSync();
|
|
236
|
+
expect(texts()).toEqual(['4', '5', '2']);
|
|
237
|
+
});
|
|
238
|
+
|
|
198
239
|
it('keyed for over derived updates sibling text nodes', () => {
|
|
199
240
|
function App() @{
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
() => Array.from({ length: count }).map((_, id) => ({ id, label: `Item ${id}` })),
|
|
241
|
+
const count = track(0);
|
|
242
|
+
const items = track(
|
|
243
|
+
() => Array.from({ length: count.value }).map((_, id) => ({ id, label: `Item ${id}` })),
|
|
203
244
|
);
|
|
204
245
|
<>
|
|
205
246
|
<button
|
|
206
247
|
onClick={() => {
|
|
207
|
-
count++;
|
|
248
|
+
count.value++;
|
|
208
249
|
}}
|
|
209
250
|
>{'Add'}</button>
|
|
210
|
-
@for (const item of items; key item.id) {
|
|
251
|
+
@for (const item of items.value; key item.id) {
|
|
211
252
|
<div class="item">{item.label}</div>
|
|
212
253
|
}
|
|
213
|
-
<p class="count">{count}</p>
|
|
254
|
+
<p class="count">{count.value}</p>
|
|
214
255
|
</>
|
|
215
256
|
}
|
|
216
257
|
|
|
@@ -236,18 +277,18 @@ describe('for statements', () => {
|
|
|
236
277
|
|
|
237
278
|
it('keyed for inserts multiple new items in the middle in correct order', () => {
|
|
238
279
|
function App() @{
|
|
239
|
-
|
|
280
|
+
const items = track([
|
|
240
281
|
{ id: 'a', text: 'A' },
|
|
241
282
|
{ id: 'c', text: 'C' },
|
|
242
283
|
{ id: 'd', text: 'D' },
|
|
243
284
|
]);
|
|
244
285
|
<>
|
|
245
|
-
@for (const item of items; key item.id) {
|
|
286
|
+
@for (const item of items.value; key item.id) {
|
|
246
287
|
<div class="item">{item.text}</div>
|
|
247
288
|
}
|
|
248
289
|
<button
|
|
249
290
|
onClick={() => {
|
|
250
|
-
items = [
|
|
291
|
+
items.value = [
|
|
251
292
|
{ id: 'a', text: 'A' },
|
|
252
293
|
{ id: 'b1', text: 'B1' },
|
|
253
294
|
{ id: 'b2', text: 'B2' },
|
|
@@ -279,14 +320,14 @@ describe('for statements', () => {
|
|
|
279
320
|
const d = { text: 'D' };
|
|
280
321
|
|
|
281
322
|
function App() @{
|
|
282
|
-
|
|
323
|
+
const items = track([a, c, d]);
|
|
283
324
|
<>
|
|
284
|
-
@for (const item of items) {
|
|
325
|
+
@for (const item of items.value) {
|
|
285
326
|
<div class="item">{item.text}</div>
|
|
286
327
|
}
|
|
287
328
|
<button
|
|
288
329
|
onClick={() => {
|
|
289
|
-
items = [a, { text: 'B1' }, { text: 'B2' }, c, d];
|
|
330
|
+
items.value = [a, { text: 'B1' }, { text: 'B2' }, c, d];
|
|
290
331
|
}}
|
|
291
332
|
>{'Insert'}</button>
|
|
292
333
|
</>
|
|
@@ -308,16 +349,16 @@ describe('for statements', () => {
|
|
|
308
349
|
|
|
309
350
|
it('keyed for with 32+ items: full reversal updates values via Map path', () => {
|
|
310
351
|
function App() @{
|
|
311
|
-
|
|
352
|
+
const items = track(Array.from({ length: 40 }, (_, i) => ({ id: i, text: `Item ${i}` })));
|
|
312
353
|
<>
|
|
313
354
|
<div>
|
|
314
|
-
@for (let item of items; index idx; key item.id) {
|
|
355
|
+
@for (let item of items.value; index idx; key item.id) {
|
|
315
356
|
<span class="item">{idx + ':' + item.text}</span>
|
|
316
357
|
}
|
|
317
358
|
</div>
|
|
318
359
|
<button
|
|
319
360
|
onClick={() => {
|
|
320
|
-
items = items.toReversed();
|
|
361
|
+
items.value = items.value.toReversed();
|
|
321
362
|
}}
|
|
322
363
|
>{'Reverse'}</button>
|
|
323
364
|
</>
|
|
@@ -339,25 +380,25 @@ describe('for statements', () => {
|
|
|
339
380
|
|
|
340
381
|
it('handles updating with new objects with same key', () => {
|
|
341
382
|
function App() @{
|
|
342
|
-
|
|
383
|
+
const items = track([
|
|
343
384
|
{ id: 1, text: 'Item 1' },
|
|
344
385
|
{ id: 2, text: 'Item 2' },
|
|
345
386
|
{ id: 3, text: 'Item 3' },
|
|
346
387
|
]);
|
|
347
388
|
<>
|
|
348
|
-
@for (let item of items; index i; key item.id) {
|
|
389
|
+
@for (let item of items.value; index i; key item.id) {
|
|
349
390
|
<div>{i + ':' + item.text}</div>
|
|
350
391
|
}
|
|
351
392
|
<button
|
|
352
393
|
onClick={() => {
|
|
353
|
-
items[0].id = 3;
|
|
354
|
-
items[1].id = 2;
|
|
355
|
-
items[2].id = 1;
|
|
356
|
-
|
|
357
|
-
items = [
|
|
358
|
-
{ ...items[0], text: 'Item 1!' },
|
|
359
|
-
{ ...items[1], text: 'Item 2!' },
|
|
360
|
-
{ ...items[2], text: 'Item 3!' },
|
|
394
|
+
items.value[0].id = 3;
|
|
395
|
+
items.value[1].id = 2;
|
|
396
|
+
items.value[2].id = 1;
|
|
397
|
+
|
|
398
|
+
items.value = [
|
|
399
|
+
{ ...items.value[0], text: 'Item 1!' },
|
|
400
|
+
{ ...items.value[1], text: 'Item 2!' },
|
|
401
|
+
{ ...items.value[2], text: 'Item 3!' },
|
|
361
402
|
];
|
|
362
403
|
}}
|
|
363
404
|
>{'Reverse'}</button>
|
|
@@ -378,16 +419,16 @@ describe('for statements', () => {
|
|
|
378
419
|
const objects = Array.from({ length: 50 }, (_, i) => ({ id: i, text: `Obj ${i}` }));
|
|
379
420
|
|
|
380
421
|
function App() @{
|
|
381
|
-
|
|
422
|
+
const items = track(objects.slice());
|
|
382
423
|
<>
|
|
383
424
|
<div>
|
|
384
|
-
@for (const item of items) {
|
|
425
|
+
@for (const item of items.value) {
|
|
385
426
|
<span class="item">{item.text}</span>
|
|
386
427
|
}
|
|
387
428
|
</div>
|
|
388
429
|
<button
|
|
389
430
|
onClick={() => {
|
|
390
|
-
items = objects.slice(15).reverse();
|
|
431
|
+
items.value = objects.slice(15).reverse();
|
|
391
432
|
}}
|
|
392
433
|
>{'Trim and reverse'}</button>
|
|
393
434
|
</>
|
|
@@ -406,4 +447,264 @@ describe('for statements', () => {
|
|
|
406
447
|
|
|
407
448
|
expect(getTexts()).toEqual(objects.slice(15).reverse().map((o) => o.text));
|
|
408
449
|
});
|
|
450
|
+
|
|
451
|
+
it('renders the index and the key from a function that returns its template', () => {
|
|
452
|
+
function Plain({ items }) {
|
|
453
|
+
return <ul>
|
|
454
|
+
@for (const item of items) {
|
|
455
|
+
<li class="plain">{item.text}</li>
|
|
456
|
+
}
|
|
457
|
+
</ul>;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function Indexed({ items }) {
|
|
461
|
+
return <ul>
|
|
462
|
+
@for (const item of items; index i) {
|
|
463
|
+
<li class="indexed">{i + ': ' + item.text}</li>
|
|
464
|
+
}
|
|
465
|
+
</ul>;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function Keyed({ items }) {
|
|
469
|
+
return <ul>
|
|
470
|
+
@for (const item of items; key item.id) {
|
|
471
|
+
<li class="keyed">{item.text}</li>
|
|
472
|
+
}
|
|
473
|
+
</ul>;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function IndexedAndKeyed({ items }) {
|
|
477
|
+
return <ul>
|
|
478
|
+
@for (const item of items; index i; key item.id) {
|
|
479
|
+
<li class="indexed-keyed">{i + ': ' + item.text}</li>
|
|
480
|
+
}
|
|
481
|
+
</ul>;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const Arrow = ({ items }) => <ul>
|
|
485
|
+
@for (const item of items; index i) {
|
|
486
|
+
<li class="arrow">{i + ': ' + item.text}</li>
|
|
487
|
+
}
|
|
488
|
+
</ul>;
|
|
489
|
+
|
|
490
|
+
function App() @{
|
|
491
|
+
const items = [
|
|
492
|
+
{ id: 1, text: 'a' },
|
|
493
|
+
{ id: 2, text: 'b' },
|
|
494
|
+
];
|
|
495
|
+
<>
|
|
496
|
+
<Plain {items} />
|
|
497
|
+
<Indexed {items} />
|
|
498
|
+
<Keyed {items} />
|
|
499
|
+
<IndexedAndKeyed {items} />
|
|
500
|
+
<Arrow {items} />
|
|
501
|
+
</>
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
render(App);
|
|
505
|
+
|
|
506
|
+
const texts = (selector) => Array.from(container.querySelectorAll(selector)).map(
|
|
507
|
+
(el) => el.textContent,
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
expect(texts('.plain')).toEqual(['a', 'b']);
|
|
511
|
+
expect(texts('.indexed')).toEqual(['0: a', '1: b']);
|
|
512
|
+
expect(texts('.keyed')).toEqual(['a', 'b']);
|
|
513
|
+
expect(texts('.indexed-keyed')).toEqual(['0: a', '1: b']);
|
|
514
|
+
expect(texts('.arrow')).toEqual(['0: a', '1: b']);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('renders the index and the key from a template held in a variable', () => {
|
|
518
|
+
function Indexed({ items }) {
|
|
519
|
+
const list = <ul>
|
|
520
|
+
@for (const item of items; index i) {
|
|
521
|
+
<li class="indexed">{i + ': ' + item.text}</li>
|
|
522
|
+
}
|
|
523
|
+
</ul>;
|
|
524
|
+
return list;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function Keyed({ items }) {
|
|
528
|
+
return [
|
|
529
|
+
<ul>
|
|
530
|
+
@for (const item of items; key item.id) {
|
|
531
|
+
<li class="keyed">{item.text}</li>
|
|
532
|
+
}
|
|
533
|
+
</ul>,
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function App() @{
|
|
538
|
+
const items = [
|
|
539
|
+
{ id: 1, text: 'a' },
|
|
540
|
+
{ id: 2, text: 'b' },
|
|
541
|
+
];
|
|
542
|
+
<>
|
|
543
|
+
<Indexed {items} />
|
|
544
|
+
<Keyed {items} />
|
|
545
|
+
</>
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
render(App);
|
|
549
|
+
|
|
550
|
+
const texts = (selector) => Array.from(container.querySelectorAll(selector)).map(
|
|
551
|
+
(el) => el.textContent,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
expect(texts('.indexed')).toEqual(['0: a', '1: b']);
|
|
555
|
+
expect(texts('.keyed')).toEqual(['a', 'b']);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('renders a bare `@for` held in a variable through an expression', () => {
|
|
559
|
+
function Returned({ items }) {
|
|
560
|
+
const yo = @for (const item of items; index i; key item.id) {
|
|
561
|
+
<li class="returned">{i + ': ' + item.text}</li>
|
|
562
|
+
};
|
|
563
|
+
return <ul>{yo}</ul>;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function Held({ items }) {
|
|
567
|
+
const yo = @for (const item of items; index i) {
|
|
568
|
+
<li class="held">{i + ': ' + item.text}</li>
|
|
569
|
+
};
|
|
570
|
+
const list = <ul>{yo}</ul>;
|
|
571
|
+
return list;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function App() @{
|
|
575
|
+
const items = [
|
|
576
|
+
{ id: 1, text: 'a' },
|
|
577
|
+
{ id: 2, text: 'b' },
|
|
578
|
+
];
|
|
579
|
+
<>
|
|
580
|
+
<Returned {items} />
|
|
581
|
+
<Held {items} />
|
|
582
|
+
</>
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
render(App);
|
|
586
|
+
|
|
587
|
+
const texts = (selector) => Array.from(container.querySelectorAll(selector)).map(
|
|
588
|
+
(el) => el.textContent,
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
expect(texts('.returned')).toEqual(['0: a', '1: b']);
|
|
592
|
+
expect(texts('.held')).toEqual(['0: a', '1: b']);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('reads rest and default pattern names off the per-key item as it changes', () => {
|
|
596
|
+
type Row = { id: number; name: string; tags?: string[] };
|
|
597
|
+
let replace: (rows: Row[]) => void = () => {};
|
|
598
|
+
|
|
599
|
+
function App() @{
|
|
600
|
+
const rows = track<Row[]>([
|
|
601
|
+
{ id: 1, name: 'one', tags: ['a', 'b'] },
|
|
602
|
+
]);
|
|
603
|
+
replace = (next) => {
|
|
604
|
+
rows.value = next;
|
|
605
|
+
};
|
|
606
|
+
<ul>
|
|
607
|
+
@for (const { id, ...rest } of rows.value; key id) {
|
|
608
|
+
<li>{rest.name}</li>
|
|
609
|
+
}
|
|
610
|
+
@for (const [head, second = 'none', ...others] of rows.value.map(
|
|
611
|
+
(row) => row.tags ?? [],
|
|
612
|
+
); key head) {
|
|
613
|
+
<li>{head + ':' + second + ':' + others.length}</li>
|
|
614
|
+
}
|
|
615
|
+
</ul>
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
render(App);
|
|
619
|
+
const items = () => [...container.querySelectorAll('li')].map((li) => li.textContent);
|
|
620
|
+
expect(items()).toEqual(['one', 'a:b:0']);
|
|
621
|
+
|
|
622
|
+
// A new object under the same key updates the existing block in place.
|
|
623
|
+
replace([
|
|
624
|
+
{ id: 1, name: 'uno', tags: ['a'] },
|
|
625
|
+
]);
|
|
626
|
+
flushSync();
|
|
627
|
+
expect(items()).toEqual(['uno', 'a:none:0']);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('destructures a keyed item once per change, so defaults run once', () => {
|
|
631
|
+
type Row = { id: number; label?: string };
|
|
632
|
+
let replace: (rows: Row[]) => void = () => {};
|
|
633
|
+
let defaults = 0;
|
|
634
|
+
const next = () => `default ${++defaults}`;
|
|
635
|
+
|
|
636
|
+
function App() @{
|
|
637
|
+
const rows = track<Row[]>([{ id: 1 }]);
|
|
638
|
+
replace = (next) => {
|
|
639
|
+
rows.value = next;
|
|
640
|
+
};
|
|
641
|
+
<ul>
|
|
642
|
+
@for (const { id, label = next() } of rows.value; key id) {
|
|
643
|
+
<li>{label + '/' + label + '/' + id}</li>
|
|
644
|
+
}
|
|
645
|
+
</ul>
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
render(App);
|
|
649
|
+
const items = () => [...container.querySelectorAll('li')].map((li) => li.textContent);
|
|
650
|
+
// Two reads of `label`, one evaluation of its default.
|
|
651
|
+
expect(items()).toEqual(['default 1/default 1/1']);
|
|
652
|
+
expect(defaults).toBe(1);
|
|
653
|
+
|
|
654
|
+
replace([{ id: 1 }]);
|
|
655
|
+
flushSync();
|
|
656
|
+
expect(items()).toEqual(['default 2/default 2/1']);
|
|
657
|
+
expect(defaults).toBe(2);
|
|
658
|
+
|
|
659
|
+
replace([
|
|
660
|
+
{ id: 1, label: 'given' },
|
|
661
|
+
]);
|
|
662
|
+
flushSync();
|
|
663
|
+
expect(items()).toEqual(['given/given/1']);
|
|
664
|
+
expect(defaults).toBe(2);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('reads rest pattern names inside a hoisted branch of a keyed loop', () => {
|
|
668
|
+
type Row = { id: number; name: string };
|
|
669
|
+
let replace: (rows: Row[]) => void = () => {};
|
|
670
|
+
let toggle: () => void = () => {};
|
|
671
|
+
|
|
672
|
+
function App() @{
|
|
673
|
+
const rows = track<Row[]>([
|
|
674
|
+
{ id: 1, name: 'one' },
|
|
675
|
+
]);
|
|
676
|
+
const show = track(true);
|
|
677
|
+
replace = (next) => {
|
|
678
|
+
rows.value = next;
|
|
679
|
+
};
|
|
680
|
+
toggle = () => {
|
|
681
|
+
show.value = !show.value;
|
|
682
|
+
};
|
|
683
|
+
<ul>
|
|
684
|
+
@for (const { id, ...rest } of rows.value; key id) {
|
|
685
|
+
@if (show.value) {
|
|
686
|
+
<li>{rest.name}</li>
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
</ul>
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
render(App);
|
|
693
|
+
const items = () => [...container.querySelectorAll('li')].map((li) => li.textContent);
|
|
694
|
+
expect(items()).toEqual(['one']);
|
|
695
|
+
|
|
696
|
+
replace([
|
|
697
|
+
{ id: 1, name: 'uno' },
|
|
698
|
+
]);
|
|
699
|
+
flushSync();
|
|
700
|
+
expect(items()).toEqual(['uno']);
|
|
701
|
+
|
|
702
|
+
toggle();
|
|
703
|
+
flushSync();
|
|
704
|
+
expect(items()).toEqual([]);
|
|
705
|
+
|
|
706
|
+
toggle();
|
|
707
|
+
flushSync();
|
|
708
|
+
expect(items()).toEqual(['uno']);
|
|
709
|
+
});
|
|
409
710
|
});
|