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.
- package/CHANGELOG.md +267 -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 +173 -91
- 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 +9 -0
- 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 +45 -25
- package/src/runtime/internal/client/for.js +409 -129
- 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 +4 -12
- package/src/runtime/internal/client/operations.js +40 -61
- package/src/runtime/internal/client/portal.js +163 -85
- package/src/runtime/internal/client/render.js +39 -39
- package/src/runtime/internal/client/rpc.js +3 -3
- package/src/runtime/internal/client/runtime.js +592 -433
- package/src/runtime/internal/client/selector.js +5 -2
- 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 +56 -23
- 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 +325 -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 +73 -38
- 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 +14 -14
- 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 +184 -171
- package/tests/hydration/compiled/client/for.js +1472 -1132
- package/tests/hydration/compiled/client/fragment-cursor.js +2341 -0
- package/tests/hydration/compiled/client/head.js +254 -201
- 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 -404
- package/tests/hydration/compiled/client/nested-control-flow.js +1326 -1277
- package/tests/hydration/compiled/client/portal.js +87 -95
- package/tests/hydration/compiled/client/reactivity.js +135 -101
- 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 +66 -18
- package/tests/hydration/compiled/server/for.js +117 -41
- package/tests/hydration/compiled/server/fragment-cursor.js +1687 -0
- package/tests/hydration/compiled/server/head.js +20 -20
- package/tests/hydration/compiled/server/hmr.js +2 -2
- package/tests/hydration/compiled/server/html.js +70 -238
- 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 -14
- 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
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import type { Derived, Tracked } from 'ripple';
|
|
2
|
+
import { effect, flushSync, track } from 'ripple';
|
|
3
|
+
|
|
4
|
+
/** The runtime's own subscriber list of a tracked value (`sb` / `sn`). */
|
|
5
|
+
function subscribers(source: Tracked<number>) {
|
|
6
|
+
let n = 0;
|
|
7
|
+
for (let d = (source as any).sb; d !== null; d = d.sn) n++;
|
|
8
|
+
return n;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function Child({ count }: { count: Derived<number> }) @{
|
|
12
|
+
<span class="child">{count.value}</span>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('derived release', () => {
|
|
16
|
+
it('releases a derived created in an @if branch when the branch toggles off', () => {
|
|
17
|
+
let toggle: () => void = () => {};
|
|
18
|
+
let bump: () => void = () => {};
|
|
19
|
+
let source: Tracked<number> | undefined;
|
|
20
|
+
|
|
21
|
+
function App() @{
|
|
22
|
+
const count = track(0);
|
|
23
|
+
const show = track(false);
|
|
24
|
+
source = count;
|
|
25
|
+
toggle = () => {
|
|
26
|
+
show.value = !show.value;
|
|
27
|
+
};
|
|
28
|
+
bump = () => {
|
|
29
|
+
count.value++;
|
|
30
|
+
};
|
|
31
|
+
<>
|
|
32
|
+
<p>{count.value}</p>
|
|
33
|
+
@if (show.value) {
|
|
34
|
+
const label = track(() => 'n=' + count.value);
|
|
35
|
+
<>
|
|
36
|
+
<span class="label">{label.value}</span>
|
|
37
|
+
<Child count={count.readOnly()} />
|
|
38
|
+
</>
|
|
39
|
+
}
|
|
40
|
+
</>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
render(App);
|
|
44
|
+
flushSync();
|
|
45
|
+
const before = subscribers(source!);
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < 5; i++) {
|
|
48
|
+
toggle();
|
|
49
|
+
flushSync();
|
|
50
|
+
expect(container.querySelector('.label')!.textContent).toBe(`n=${i}`);
|
|
51
|
+
expect(subscribers(source!)).toBe(before + 2);
|
|
52
|
+
toggle();
|
|
53
|
+
flushSync();
|
|
54
|
+
// Both deriveds of the old branch are gone before the next write.
|
|
55
|
+
expect(subscribers(source!)).toBe(before);
|
|
56
|
+
bump();
|
|
57
|
+
flushSync();
|
|
58
|
+
expect(subscribers(source!)).toBe(before);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('releases deriveds created in keyed @for items when the items are removed', () => {
|
|
63
|
+
let set_rows: (rows: number[]) => void = () => {};
|
|
64
|
+
let source: Tracked<number> | undefined;
|
|
65
|
+
|
|
66
|
+
function App() @{
|
|
67
|
+
const count = track(0);
|
|
68
|
+
const rows = track<number[]>([]);
|
|
69
|
+
source = count;
|
|
70
|
+
set_rows = (next) => {
|
|
71
|
+
rows.value = next;
|
|
72
|
+
};
|
|
73
|
+
<ul>
|
|
74
|
+
@for (const row of rows.value; key row) {
|
|
75
|
+
<li>
|
|
76
|
+
<Child count={count.readOnly()} />
|
|
77
|
+
</li>
|
|
78
|
+
}
|
|
79
|
+
</ul>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render(App);
|
|
83
|
+
flushSync();
|
|
84
|
+
set_rows([1, 2, 3]);
|
|
85
|
+
flushSync();
|
|
86
|
+
expect(subscribers(source!)).toBe(3);
|
|
87
|
+
|
|
88
|
+
set_rows([2]);
|
|
89
|
+
flushSync();
|
|
90
|
+
expect(subscribers(source!)).toBe(1);
|
|
91
|
+
|
|
92
|
+
set_rows([]);
|
|
93
|
+
flushSync();
|
|
94
|
+
expect(subscribers(source!)).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('releases a derived created in component setup when the component unmounts', () => {
|
|
98
|
+
let toggle: () => void = () => {};
|
|
99
|
+
let source: Tracked<number> | undefined;
|
|
100
|
+
|
|
101
|
+
function Doubled({ count }: { count: Tracked<number> }) @{
|
|
102
|
+
const double = track(() => count.value * 2);
|
|
103
|
+
<span class="double">{double.value}</span>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function App() @{
|
|
107
|
+
const count = track(1);
|
|
108
|
+
const show = track(true);
|
|
109
|
+
source = count;
|
|
110
|
+
toggle = () => {
|
|
111
|
+
show.value = !show.value;
|
|
112
|
+
};
|
|
113
|
+
<>
|
|
114
|
+
@if (show.value) {
|
|
115
|
+
<Doubled {count} />
|
|
116
|
+
}
|
|
117
|
+
</>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
render(App);
|
|
121
|
+
flushSync();
|
|
122
|
+
expect(container.querySelector('.double')!.textContent).toBe('2');
|
|
123
|
+
expect(subscribers(source!)).toBe(1);
|
|
124
|
+
|
|
125
|
+
toggle();
|
|
126
|
+
flushSync();
|
|
127
|
+
expect(container.querySelector('.double')).toBeNull();
|
|
128
|
+
expect(subscribers(source!)).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('keeps a derived that a longer-lived reader still holds', () => {
|
|
132
|
+
let toggle: () => void = () => {};
|
|
133
|
+
let bump: () => void = () => {};
|
|
134
|
+
|
|
135
|
+
function App() @{
|
|
136
|
+
const count = track(1);
|
|
137
|
+
const show = track(true);
|
|
138
|
+
const held = track<Derived<number> | null>(null);
|
|
139
|
+
toggle = () => {
|
|
140
|
+
show.value = !show.value;
|
|
141
|
+
};
|
|
142
|
+
bump = () => {
|
|
143
|
+
count.value++;
|
|
144
|
+
};
|
|
145
|
+
<>
|
|
146
|
+
<p class="outer">
|
|
147
|
+
{held.value === null ? '-' : held.value.value}
|
|
148
|
+
</p>
|
|
149
|
+
@if (show.value) {
|
|
150
|
+
const double = track(() => count.value * 2);
|
|
151
|
+
held.value = double;
|
|
152
|
+
<span class="inner">{double.value}</span>
|
|
153
|
+
}
|
|
154
|
+
</>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
render(App);
|
|
158
|
+
flushSync();
|
|
159
|
+
expect(container.querySelector('.outer')!.textContent).toBe('2');
|
|
160
|
+
|
|
161
|
+
// The branch that created the derived is gone, but the outer text still
|
|
162
|
+
// reads it, so it stays subscribed and keeps following the source.
|
|
163
|
+
toggle();
|
|
164
|
+
flushSync();
|
|
165
|
+
expect(container.querySelector('.inner')).toBeNull();
|
|
166
|
+
bump();
|
|
167
|
+
flushSync();
|
|
168
|
+
expect(container.querySelector('.outer')!.textContent).toBe('4');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('derived release across reruns', () => {
|
|
173
|
+
it('keeps track of deriveds across reruns that stay on the same branch', () => {
|
|
174
|
+
let bump: () => void = () => {};
|
|
175
|
+
let reset: () => void = () => {};
|
|
176
|
+
let source: Tracked<number> | undefined;
|
|
177
|
+
|
|
178
|
+
function App() @{
|
|
179
|
+
const count = track(1);
|
|
180
|
+
source = count;
|
|
181
|
+
bump = () => {
|
|
182
|
+
count.value++;
|
|
183
|
+
};
|
|
184
|
+
reset = () => {
|
|
185
|
+
count.value = 0;
|
|
186
|
+
};
|
|
187
|
+
<>
|
|
188
|
+
@if (count.value > 0) {
|
|
189
|
+
const double = track(() => count.value * 2);
|
|
190
|
+
<p class="double">{double.value}</p>
|
|
191
|
+
}
|
|
192
|
+
</>
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
render(App);
|
|
196
|
+
flushSync();
|
|
197
|
+
// The if block reads `count` for its condition; the derived is the other subscriber.
|
|
198
|
+
const before = subscribers(source!) - 1;
|
|
199
|
+
|
|
200
|
+
// Same branch on every rerun: nothing is created, nothing accumulates.
|
|
201
|
+
for (let i = 0; i < 4; i++) {
|
|
202
|
+
bump();
|
|
203
|
+
flushSync();
|
|
204
|
+
expect(container.querySelector('.double')!.textContent).toBe(String((i + 2) * 2));
|
|
205
|
+
expect(subscribers(source!)).toBe(before + 1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The branch finally goes away: its derived goes with it.
|
|
209
|
+
reset();
|
|
210
|
+
flushSync();
|
|
211
|
+
expect(container.querySelector('.double')).toBeNull();
|
|
212
|
+
expect(subscribers(source!)).toBe(before);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('releases a derived a render expression created and read on its previous run', () => {
|
|
216
|
+
let bump: () => void = () => {};
|
|
217
|
+
let source: Tracked<number> | undefined;
|
|
218
|
+
|
|
219
|
+
function App() @{
|
|
220
|
+
const count = track(0);
|
|
221
|
+
source = count;
|
|
222
|
+
bump = () => {
|
|
223
|
+
count.value++;
|
|
224
|
+
};
|
|
225
|
+
<p class="value">{track(() => count.value * 10).value}</p>
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
render(App);
|
|
229
|
+
flushSync();
|
|
230
|
+
expect(container.querySelector('.value')!.textContent).toBe('0');
|
|
231
|
+
expect(subscribers(source!)).toBe(1);
|
|
232
|
+
|
|
233
|
+
// Each rerun creates a new derived and the previous one is released, so
|
|
234
|
+
// the source never sees more than the current one.
|
|
235
|
+
for (let i = 1; i <= 5; i++) {
|
|
236
|
+
bump();
|
|
237
|
+
flushSync();
|
|
238
|
+
expect(container.querySelector('.value')!.textContent).toBe(String(i * 10));
|
|
239
|
+
expect(subscribers(source!)).toBe(1);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('releases a chain of deriveds after reruns that kept them, whatever their count', () => {
|
|
244
|
+
let bump: () => void = () => {};
|
|
245
|
+
let reset: () => void = () => {};
|
|
246
|
+
let source: Tracked<number> | undefined;
|
|
247
|
+
|
|
248
|
+
function App() @{
|
|
249
|
+
const count = track(1);
|
|
250
|
+
source = count;
|
|
251
|
+
bump = () => {
|
|
252
|
+
count.value++;
|
|
253
|
+
};
|
|
254
|
+
reset = () => {
|
|
255
|
+
count.value = 0;
|
|
256
|
+
};
|
|
257
|
+
<>
|
|
258
|
+
@if (count.value > 0) {
|
|
259
|
+
// `second` reads `first`; a release must visit `second` before
|
|
260
|
+
// `first`, in every pass, or `first` looks alive and survives.
|
|
261
|
+
const first = track(() => count.value * 2);
|
|
262
|
+
const second = track(() => first.value + 1);
|
|
263
|
+
<p class="second">{second.value}</p>
|
|
264
|
+
}
|
|
265
|
+
</>
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
render(App);
|
|
269
|
+
flushSync();
|
|
270
|
+
const before = subscribers(source!) - 1;
|
|
271
|
+
|
|
272
|
+
for (const same_branch_reruns of [1, 3, 2]) {
|
|
273
|
+
for (let i = 0; i < same_branch_reruns; i++) {
|
|
274
|
+
bump();
|
|
275
|
+
flushSync();
|
|
276
|
+
}
|
|
277
|
+
expect(subscribers(source!)).toBe(before + 1);
|
|
278
|
+
reset();
|
|
279
|
+
flushSync();
|
|
280
|
+
expect(container.querySelector('.second')).toBeNull();
|
|
281
|
+
expect(subscribers(source!)).toBe(before);
|
|
282
|
+
bump();
|
|
283
|
+
flushSync();
|
|
284
|
+
expect(container.querySelector('.second')!.textContent).toBe('3');
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('releases a derived an effect created and read on its previous run', () => {
|
|
289
|
+
let bump: () => void = () => {};
|
|
290
|
+
let source: Tracked<number> | undefined;
|
|
291
|
+
const seen: number[] = [];
|
|
292
|
+
|
|
293
|
+
function App() @{
|
|
294
|
+
const count = track(0);
|
|
295
|
+
source = count;
|
|
296
|
+
bump = () => {
|
|
297
|
+
count.value++;
|
|
298
|
+
};
|
|
299
|
+
effect(() => {
|
|
300
|
+
// On even counts the run makes a derived and reads it; on odd counts
|
|
301
|
+
// it does not, so the block's stale link to the previous run's
|
|
302
|
+
// derived is still in place when that derived is released. It must
|
|
303
|
+
// not count as a live reader.
|
|
304
|
+
const n = count.value;
|
|
305
|
+
if (n % 2 === 0) {
|
|
306
|
+
const tenfold = track(() => count.value * 10);
|
|
307
|
+
seen.push(tenfold.value);
|
|
308
|
+
} else {
|
|
309
|
+
seen.push(-1);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
<p>{count.value}</p>
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
render(App);
|
|
316
|
+
flushSync();
|
|
317
|
+
expect(seen).toEqual([0]);
|
|
318
|
+
const before = subscribers(source!);
|
|
319
|
+
|
|
320
|
+
for (let i = 1; i <= 6; i++) {
|
|
321
|
+
bump();
|
|
322
|
+
flushSync();
|
|
323
|
+
expect(seen.at(-1)).toBe(i % 2 === 0 ? i * 10 : -1);
|
|
324
|
+
// Only the current run's derived, if any, is subscribed.
|
|
325
|
+
expect(subscribers(source!)).toBe(before - i % 2);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('still releases deriveds when the rerun throws into a catch boundary', () => {
|
|
330
|
+
let fail: () => void = () => {};
|
|
331
|
+
let source: Tracked<number> | undefined;
|
|
332
|
+
|
|
333
|
+
function App() @{
|
|
334
|
+
const count = track(1);
|
|
335
|
+
const broken = track(false);
|
|
336
|
+
source = count;
|
|
337
|
+
fail = () => {
|
|
338
|
+
broken.value = true;
|
|
339
|
+
};
|
|
340
|
+
<>
|
|
341
|
+
@try {
|
|
342
|
+
@if (broken.value ? boom() : count.value > 0) {
|
|
343
|
+
const double = track(() => count.value * 2);
|
|
344
|
+
<p class="double">{double.value}</p>
|
|
345
|
+
}
|
|
346
|
+
} @catch (error) {
|
|
347
|
+
<p class="error">{'failed'}</p>
|
|
348
|
+
}
|
|
349
|
+
</>
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function boom(): boolean {
|
|
353
|
+
throw new Error('condition failed');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
render(App);
|
|
357
|
+
flushSync();
|
|
358
|
+
expect(container.querySelector('.double')!.textContent).toBe('2');
|
|
359
|
+
const with_branch = subscribers(source!);
|
|
360
|
+
|
|
361
|
+
// The if block's rerun throws before it can release anything; the
|
|
362
|
+
// boundary then tears the if block down, which must still find them.
|
|
363
|
+
fail();
|
|
364
|
+
flushSync();
|
|
365
|
+
expect(container.querySelector('.error')).not.toBeNull();
|
|
366
|
+
expect(container.querySelector('.double')).toBeNull();
|
|
367
|
+
expect(subscribers(source!)).toBeLessThan(with_branch);
|
|
368
|
+
expect(subscribers(source!)).toBe(0);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PropsWithExtras } from 'ripple';
|
|
1
|
+
import type { Derived, PropsWithExtras } from 'ripple';
|
|
2
2
|
import { createRefKey, effect, flushSync, track } from 'ripple';
|
|
3
3
|
|
|
4
4
|
describe('dynamic DOM elements', () => {
|
|
@@ -57,14 +57,14 @@ describe('dynamic DOM elements', () => {
|
|
|
57
57
|
|
|
58
58
|
it('renders reactive dynamic element', () => {
|
|
59
59
|
function App() @{
|
|
60
|
-
|
|
60
|
+
const tag = track('div');
|
|
61
61
|
<>
|
|
62
62
|
<button
|
|
63
63
|
onClick={() => {
|
|
64
|
-
tag = 'span';
|
|
64
|
+
tag.value = 'span';
|
|
65
65
|
}}
|
|
66
66
|
>{'Change Tag'}</button>
|
|
67
|
-
<{tag} id="dynamic">{'Hello World'}</{tag}>
|
|
67
|
+
<{tag.value} id="dynamic">{'Hello World'}</{tag.value}>
|
|
68
68
|
</>
|
|
69
69
|
}
|
|
70
70
|
render(App);
|
|
@@ -101,8 +101,8 @@ describe('dynamic DOM elements', () => {
|
|
|
101
101
|
it('handles dynamic element with attributes', () => {
|
|
102
102
|
function App() @{
|
|
103
103
|
let tag = track('div');
|
|
104
|
-
|
|
105
|
-
<{tag} class={className} id="test" data-testid="dynamic-element">{'Content'}</{tag}>
|
|
104
|
+
const className = track('test-class');
|
|
105
|
+
<{tag} class={className.value} id="test" data-testid="dynamic-element">{'Content'}</{tag}>
|
|
106
106
|
}
|
|
107
107
|
render(App);
|
|
108
108
|
|
|
@@ -135,9 +135,9 @@ describe('dynamic DOM elements', () => {
|
|
|
135
135
|
it('handles dynamic element with class object', () => {
|
|
136
136
|
function App() @{
|
|
137
137
|
let tag = track('div');
|
|
138
|
-
|
|
138
|
+
const active = track(true);
|
|
139
139
|
<{tag}
|
|
140
|
-
class={{ active: active, 'dynamic-element': true }}
|
|
140
|
+
class={{ active: active.value, 'dynamic-element': true }}
|
|
141
141
|
>{'Element with class object'}</{tag}>
|
|
142
142
|
}
|
|
143
143
|
render(App);
|
|
@@ -301,14 +301,14 @@ describe('dynamic DOM elements', () => {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
function App() @{
|
|
304
|
-
|
|
304
|
+
const dynamic = track(() => TextInput);
|
|
305
305
|
<>
|
|
306
306
|
<button
|
|
307
307
|
onClick={() => {
|
|
308
|
-
dynamic = dynamic === TextInput ? SearchInput : TextInput;
|
|
308
|
+
dynamic.value = dynamic.value === TextInput ? SearchInput : TextInput;
|
|
309
309
|
}}
|
|
310
310
|
>{'Change Component'}</button>
|
|
311
|
-
<{dynamic}
|
|
311
|
+
<{dynamic.value}
|
|
312
312
|
ref={[
|
|
313
313
|
(node: HTMLInputElement | null) => {
|
|
314
314
|
refAttrElement = node;
|
|
@@ -375,20 +375,20 @@ describe('dynamic DOM elements', () => {
|
|
|
375
375
|
it('has reactive attributes on dynamic elements', () => {
|
|
376
376
|
function App() @{
|
|
377
377
|
let tag = track('div');
|
|
378
|
-
|
|
378
|
+
const count = track(0);
|
|
379
379
|
<>
|
|
380
380
|
<button
|
|
381
381
|
onClick={() => {
|
|
382
|
-
count++;
|
|
382
|
+
count.value++;
|
|
383
383
|
}}
|
|
384
384
|
>{'Increment'}</button>
|
|
385
385
|
<{tag}
|
|
386
|
-
id={count % 2 ? 'even' : 'odd'}
|
|
387
|
-
class={count % 2 ? 'even-class' : 'odd-class'}
|
|
388
|
-
data-count={count}
|
|
386
|
+
id={count.value % 2 ? 'even' : 'odd'}
|
|
387
|
+
class={count.value % 2 ? 'even-class' : 'odd-class'}
|
|
388
|
+
data-count={count.value}
|
|
389
389
|
>
|
|
390
390
|
{'Count: '}
|
|
391
|
-
{count}
|
|
391
|
+
{count.value}
|
|
392
392
|
</{tag}>
|
|
393
393
|
</>
|
|
394
394
|
}
|
|
@@ -453,17 +453,17 @@ describe('dynamic DOM elements', () => {
|
|
|
453
453
|
it('applies scoped CSS to dynamic elements with reactive classes', () => {
|
|
454
454
|
function App() @{
|
|
455
455
|
let tag = track('button');
|
|
456
|
-
|
|
456
|
+
const count = track(0);
|
|
457
457
|
<>
|
|
458
458
|
<{tag}
|
|
459
|
-
class={count % 2 ? 'even' : 'odd'}
|
|
460
|
-
id={count % 2 ? 'even' : 'odd'}
|
|
459
|
+
class={count.value % 2 ? 'even' : 'odd'}
|
|
460
|
+
id={count.value % 2 ? 'even' : 'odd'}
|
|
461
461
|
onClick={() => {
|
|
462
|
-
count++;
|
|
462
|
+
count.value++;
|
|
463
463
|
}}
|
|
464
464
|
>
|
|
465
465
|
{'Count: '}
|
|
466
|
-
{count}
|
|
466
|
+
{count.value}
|
|
467
467
|
</{tag}>
|
|
468
468
|
<style>
|
|
469
469
|
.even {
|
|
@@ -521,14 +521,14 @@ describe('dynamic DOM elements', () => {
|
|
|
521
521
|
});
|
|
522
522
|
|
|
523
523
|
it('handles spread attributes with class and CSS scoping ', () => {
|
|
524
|
-
function DynamicButton(
|
|
525
|
-
class: string
|
|
526
|
-
id: string
|
|
524
|
+
function DynamicButton({ ...rest }: PropsWithExtras<{
|
|
525
|
+
class: Derived<string>;
|
|
526
|
+
id: Derived<string>;
|
|
527
527
|
onClick: EventListener;
|
|
528
528
|
}>) @{
|
|
529
529
|
const tag = track('button');
|
|
530
530
|
<>
|
|
531
|
-
<{tag} {...rest}>{rest.class}</{tag}>
|
|
531
|
+
<{tag} {...rest}>{rest.class.value}</{tag}>
|
|
532
532
|
<style>
|
|
533
533
|
.even {
|
|
534
534
|
background-color: green;
|
|
@@ -541,12 +541,12 @@ describe('dynamic DOM elements', () => {
|
|
|
541
541
|
}
|
|
542
542
|
|
|
543
543
|
function App() @{
|
|
544
|
-
|
|
544
|
+
const count = track(0);
|
|
545
545
|
<DynamicButton
|
|
546
|
-
class={count % 2 ? 'even' : 'odd'}
|
|
547
|
-
id={count % 2 ? 'even' : 'odd'}
|
|
546
|
+
class={track(() => (count.value % 2 ? 'even' : 'odd'))}
|
|
547
|
+
id={track(() => (count.value % 2 ? 'even' : 'odd'))}
|
|
548
548
|
onClick={() => {
|
|
549
|
-
count++;
|
|
549
|
+
count.value++;
|
|
550
550
|
}}
|
|
551
551
|
/>
|
|
552
552
|
}
|
|
@@ -724,11 +724,11 @@ describe('dynamic DOM elements', () => {
|
|
|
724
724
|
}
|
|
725
725
|
|
|
726
726
|
function App() @{
|
|
727
|
-
|
|
727
|
+
const active = track(false);
|
|
728
728
|
<Button
|
|
729
|
-
data-active={String(active)}
|
|
729
|
+
data-active={track(() => String(active.value))}
|
|
730
730
|
onClick={() => {
|
|
731
|
-
active = !active;
|
|
731
|
+
active.value = !active.value;
|
|
732
732
|
}}
|
|
733
733
|
ref={(el: HTMLElement) => {
|
|
734
734
|
capturedElement = el;
|
|
@@ -764,23 +764,23 @@ describe('dynamic DOM elements', () => {
|
|
|
764
764
|
}
|
|
765
765
|
|
|
766
766
|
function App() @{
|
|
767
|
-
|
|
768
|
-
|
|
767
|
+
const active = track(false);
|
|
768
|
+
const buttonProps = track(
|
|
769
769
|
() => ({
|
|
770
|
-
'data-active': active,
|
|
770
|
+
'data-active': active.value,
|
|
771
771
|
}),
|
|
772
772
|
);
|
|
773
773
|
<Button
|
|
774
|
-
{...buttonProps}
|
|
774
|
+
{...buttonProps.value}
|
|
775
775
|
onClick={() => {
|
|
776
|
-
active = !active;
|
|
776
|
+
active.value = !active.value;
|
|
777
777
|
}}
|
|
778
778
|
ref={(el: HTMLElement) => {
|
|
779
779
|
capturedElement = el;
|
|
780
780
|
}}
|
|
781
781
|
>
|
|
782
782
|
{'content: '}
|
|
783
|
-
{active}
|
|
783
|
+
{active.value}
|
|
784
784
|
</Button>
|
|
785
785
|
}
|
|
786
786
|
|
|
@@ -809,11 +809,11 @@ describe('dynamic DOM elements', () => {
|
|
|
809
809
|
}
|
|
810
810
|
|
|
811
811
|
function App() @{
|
|
812
|
-
|
|
812
|
+
const active = track(false);
|
|
813
813
|
<Button
|
|
814
|
-
data-active={String(active)}
|
|
814
|
+
data-active={track(() => String(active.value))}
|
|
815
815
|
onClick={() => {
|
|
816
|
-
active = !active;
|
|
816
|
+
active.value = !active.value;
|
|
817
817
|
}}
|
|
818
818
|
ref={(el: HTMLElement) => {
|
|
819
819
|
capturedElement = el;
|
|
@@ -843,14 +843,14 @@ describe('dynamic DOM elements', () => {
|
|
|
843
843
|
|
|
844
844
|
it('should remove and add back a text node in a conditional statement with a tracked', () => {
|
|
845
845
|
function App() @{
|
|
846
|
-
|
|
846
|
+
const b = track(true);
|
|
847
847
|
<>
|
|
848
848
|
<div>
|
|
849
|
-
@if (b) {
|
|
849
|
+
@if (b.value) {
|
|
850
850
|
<>Inside if</>
|
|
851
851
|
}
|
|
852
852
|
</div>
|
|
853
|
-
<button onClick={() => (b = !b)}>{'Toggle b'}</button>
|
|
853
|
+
<button onClick={() => (b.value = !b.value)}>{'Toggle b'}</button>
|
|
854
854
|
</>
|
|
855
855
|
}
|
|
856
856
|
|