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,20 +1,20 @@
|
|
|
1
1
|
import type { OnEventListenerRemover } from 'ripple';
|
|
2
|
-
import { effect, flushSync, on, Portal, track } from 'ripple';
|
|
2
|
+
import { effect, flushSync, mount, on, Portal, track } from 'ripple';
|
|
3
3
|
|
|
4
4
|
describe('on() event handler', () => {
|
|
5
5
|
it('should attach multiple handlers via onClick attribute (delegated)', () => {
|
|
6
6
|
function Basic() @{
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const count1 = track(0);
|
|
8
|
+
const count2 = track(0);
|
|
9
9
|
<>
|
|
10
10
|
<button
|
|
11
11
|
onClick={() => {
|
|
12
|
-
count1++;
|
|
13
|
-
count2++;
|
|
12
|
+
count1.value++;
|
|
13
|
+
count2.value++;
|
|
14
14
|
}}
|
|
15
15
|
>{'Click me'}</button>
|
|
16
|
-
<div class="count1">{count1}</div>
|
|
17
|
-
<div class="count2">{count2}</div>
|
|
16
|
+
<div class="count1">{count1.value}</div>
|
|
17
|
+
<div class="count2">{count2.value}</div>
|
|
18
18
|
</>
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -67,16 +67,16 @@ describe('on() event handler', () => {
|
|
|
67
67
|
|
|
68
68
|
it('should attach and remove a single event handler', () => {
|
|
69
69
|
function Basic() @{
|
|
70
|
-
|
|
70
|
+
const count = track(0);
|
|
71
71
|
const setupListener = (node: HTMLButtonElement) => {
|
|
72
72
|
const remove = on(node, 'click', () => {
|
|
73
|
-
count++;
|
|
73
|
+
count.value++;
|
|
74
74
|
});
|
|
75
75
|
return remove;
|
|
76
76
|
};
|
|
77
77
|
<>
|
|
78
78
|
<button ref={setupListener}>{'Click me'}</button>
|
|
79
|
-
<div class="count">{count}</div>
|
|
79
|
+
<div class="count">{count.value}</div>
|
|
80
80
|
</>
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -99,14 +99,14 @@ describe('on() event handler', () => {
|
|
|
99
99
|
|
|
100
100
|
it('should handle multiple different event types on same element', () => {
|
|
101
101
|
function Basic() @{
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
const clickCount = track(0);
|
|
103
|
+
const mousedownCount = track(0);
|
|
104
104
|
const setupListeners = (node: HTMLButtonElement) => {
|
|
105
105
|
const remove1 = on(node, 'click', () => {
|
|
106
|
-
clickCount++;
|
|
106
|
+
clickCount.value++;
|
|
107
107
|
});
|
|
108
108
|
const remove2 = on(node, 'mousedown', () => {
|
|
109
|
-
mousedownCount++;
|
|
109
|
+
mousedownCount.value++;
|
|
110
110
|
});
|
|
111
111
|
return () => {
|
|
112
112
|
remove1();
|
|
@@ -115,8 +115,8 @@ describe('on() event handler', () => {
|
|
|
115
115
|
};
|
|
116
116
|
<>
|
|
117
117
|
<button ref={setupListeners}>{'Test'}</button>
|
|
118
|
-
<div class="click-count">{clickCount}</div>
|
|
119
|
-
<div class="mousedown-count">{mousedownCount}</div>
|
|
118
|
+
<div class="click-count">{clickCount.value}</div>
|
|
119
|
+
<div class="mousedown-count">{mousedownCount.value}</div>
|
|
120
120
|
</>
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -149,16 +149,16 @@ describe('on() event handler', () => {
|
|
|
149
149
|
|
|
150
150
|
it('should handle multiple handlers for same event type on same element', () => {
|
|
151
151
|
function Basic() @{
|
|
152
|
-
|
|
152
|
+
const callOrder = track<number[]>([]);
|
|
153
153
|
const setupListeners = (node: HTMLButtonElement) => {
|
|
154
154
|
const remove1 = on(node, 'click', () => {
|
|
155
|
-
callOrder = [...callOrder, 1];
|
|
155
|
+
callOrder.value = [...callOrder.value, 1];
|
|
156
156
|
});
|
|
157
157
|
const remove2 = on(node, 'click', () => {
|
|
158
|
-
callOrder = [...callOrder, 2];
|
|
158
|
+
callOrder.value = [...callOrder.value, 2];
|
|
159
159
|
});
|
|
160
160
|
const remove3 = on(node, 'click', () => {
|
|
161
|
-
callOrder = [...callOrder, 3];
|
|
161
|
+
callOrder.value = [...callOrder.value, 3];
|
|
162
162
|
});
|
|
163
163
|
return () => {
|
|
164
164
|
remove1();
|
|
@@ -168,7 +168,7 @@ describe('on() event handler', () => {
|
|
|
168
168
|
};
|
|
169
169
|
<>
|
|
170
170
|
<button ref={setupListeners}>{'Click me'}</button>
|
|
171
|
-
<div class="order">{callOrder.join(',')}</div>
|
|
171
|
+
<div class="order">{callOrder.value.join(',')}</div>
|
|
172
172
|
</>
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -192,19 +192,19 @@ describe('on() event handler', () => {
|
|
|
192
192
|
|
|
193
193
|
it('should remove specific handler without affecting others', () => {
|
|
194
194
|
function Basic() @{
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
const handler1Called = track(0);
|
|
196
|
+
const handler2Called = track(0);
|
|
197
|
+
const handler3Called = track(0);
|
|
198
198
|
let removeHandler2: OnEventListenerRemover | undefined;
|
|
199
199
|
const setupListeners = (node: HTMLButtonElement) => {
|
|
200
200
|
const remove1 = on(node, 'click', () => {
|
|
201
|
-
handler1Called++;
|
|
201
|
+
handler1Called.value++;
|
|
202
202
|
});
|
|
203
203
|
removeHandler2 = on(node, 'click', () => {
|
|
204
|
-
handler2Called++;
|
|
204
|
+
handler2Called.value++;
|
|
205
205
|
});
|
|
206
206
|
const remove3 = on(node, 'click', () => {
|
|
207
|
-
handler3Called++;
|
|
207
|
+
handler3Called.value++;
|
|
208
208
|
});
|
|
209
209
|
return () => {
|
|
210
210
|
remove1();
|
|
@@ -221,9 +221,9 @@ describe('on() event handler', () => {
|
|
|
221
221
|
removeHandler2 = undefined;
|
|
222
222
|
}}
|
|
223
223
|
>{'Remove handler 2'}</button>
|
|
224
|
-
<div class="h1">{handler1Called}</div>
|
|
225
|
-
<div class="h2">{handler2Called}</div>
|
|
226
|
-
<div class="h3">{handler3Called}</div>
|
|
224
|
+
<div class="h1">{handler1Called.value}</div>
|
|
225
|
+
<div class="h2">{handler2Called.value}</div>
|
|
226
|
+
<div class="h3">{handler3Called.value}</div>
|
|
227
227
|
</div>
|
|
228
228
|
}
|
|
229
229
|
|
|
@@ -266,17 +266,17 @@ describe('on() event handler', () => {
|
|
|
266
266
|
'should handle change event with multiple handlers (like bindChecked and bindIndeterminate)',
|
|
267
267
|
() => {
|
|
268
268
|
function Basic() @{
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
const checked = track(false);
|
|
270
|
+
const indeterminate = track(true);
|
|
271
271
|
const setupListeners = (node: HTMLInputElement) => {
|
|
272
|
-
node.indeterminate = indeterminate;
|
|
273
|
-
node.checked = checked;
|
|
272
|
+
node.indeterminate = indeterminate.value;
|
|
273
|
+
node.checked = checked.value;
|
|
274
274
|
|
|
275
275
|
const remove1 = on(node, 'change', () => {
|
|
276
|
-
checked = node.checked;
|
|
276
|
+
checked.value = node.checked;
|
|
277
277
|
});
|
|
278
278
|
const remove2 = on(node, 'change', () => {
|
|
279
|
-
indeterminate = node.indeterminate;
|
|
279
|
+
indeterminate.value = node.indeterminate;
|
|
280
280
|
});
|
|
281
281
|
return () => {
|
|
282
282
|
remove1();
|
|
@@ -286,10 +286,10 @@ describe('on() event handler', () => {
|
|
|
286
286
|
<div>
|
|
287
287
|
<input type="checkbox" ref={setupListeners} />
|
|
288
288
|
<div class="checked">
|
|
289
|
-
{checked ? 'true' : 'false'}
|
|
289
|
+
{checked.value ? 'true' : 'false'}
|
|
290
290
|
</div>
|
|
291
291
|
<div class="indeterminate">
|
|
292
|
-
{indeterminate ? 'true' : 'false'}
|
|
292
|
+
{indeterminate.value ? 'true' : 'false'}
|
|
293
293
|
</div>
|
|
294
294
|
</div>
|
|
295
295
|
}
|
|
@@ -317,16 +317,16 @@ describe('on() event handler', () => {
|
|
|
317
317
|
|
|
318
318
|
it('should support non-delegated events', () => {
|
|
319
319
|
function Basic() @{
|
|
320
|
-
|
|
320
|
+
const focusCount = track(0);
|
|
321
321
|
const setupListener = (node: HTMLInputElement) => {
|
|
322
322
|
const remove = on(node, 'focus', () => {
|
|
323
|
-
focusCount++;
|
|
323
|
+
focusCount.value++;
|
|
324
324
|
});
|
|
325
325
|
return remove;
|
|
326
326
|
};
|
|
327
327
|
<>
|
|
328
328
|
<input ref={setupListener} />
|
|
329
|
-
<div class="focus-count">{focusCount}</div>
|
|
329
|
+
<div class="focus-count">{focusCount.value}</div>
|
|
330
330
|
</>
|
|
331
331
|
}
|
|
332
332
|
|
|
@@ -349,19 +349,19 @@ describe('on() event handler', () => {
|
|
|
349
349
|
|
|
350
350
|
it('should handle removal of all handlers for same event type', () => {
|
|
351
351
|
function Basic() @{
|
|
352
|
-
|
|
352
|
+
const count = track(0);
|
|
353
353
|
let remove1: OnEventListenerRemover | undefined;
|
|
354
354
|
let remove2: OnEventListenerRemover | undefined;
|
|
355
355
|
let remove3: OnEventListenerRemover | undefined;
|
|
356
356
|
const setupListeners = (node: HTMLButtonElement) => {
|
|
357
357
|
remove1 = on(node, 'click', () => {
|
|
358
|
-
count++;
|
|
358
|
+
count.value++;
|
|
359
359
|
});
|
|
360
360
|
remove2 = on(node, 'click', () => {
|
|
361
|
-
count += 10;
|
|
361
|
+
count.value += 10;
|
|
362
362
|
});
|
|
363
363
|
remove3 = on(node, 'click', () => {
|
|
364
|
-
count += 100;
|
|
364
|
+
count.value += 100;
|
|
365
365
|
});
|
|
366
366
|
return () => {
|
|
367
367
|
remove1?.();
|
|
@@ -380,7 +380,7 @@ describe('on() event handler', () => {
|
|
|
380
380
|
remove1 = remove2 = remove3 = undefined;
|
|
381
381
|
}}
|
|
382
382
|
>{'Remove all'}</button>
|
|
383
|
-
<div class="count">{count}</div>
|
|
383
|
+
<div class="count">{count.value}</div>
|
|
384
384
|
</div>
|
|
385
385
|
}
|
|
386
386
|
|
|
@@ -410,9 +410,9 @@ describe('on() event handler', () => {
|
|
|
410
410
|
|
|
411
411
|
it('should not add duplicate handlers when same handler is attached multiple times', () => {
|
|
412
412
|
function Basic() @{
|
|
413
|
-
|
|
413
|
+
const count = track(0);
|
|
414
414
|
const sharedHandler = () => {
|
|
415
|
-
count++;
|
|
415
|
+
count.value++;
|
|
416
416
|
};
|
|
417
417
|
const setupListeners = (node: HTMLButtonElement) => {
|
|
418
418
|
// Attach the same handler multiple times
|
|
@@ -428,7 +428,7 @@ describe('on() event handler', () => {
|
|
|
428
428
|
};
|
|
429
429
|
<>
|
|
430
430
|
<button ref={setupListeners}>{'Click me'}</button>
|
|
431
|
-
<div class="count">{count}</div>
|
|
431
|
+
<div class="count">{count.value}</div>
|
|
432
432
|
</>
|
|
433
433
|
}
|
|
434
434
|
|
|
@@ -452,9 +452,9 @@ describe('on() event handler', () => {
|
|
|
452
452
|
|
|
453
453
|
it('should allow duplicate handlers when delegated is false (no deduplication)', () => {
|
|
454
454
|
function Basic() @{
|
|
455
|
-
|
|
455
|
+
const count = track(0);
|
|
456
456
|
const sharedHandler = () => {
|
|
457
|
-
count++;
|
|
457
|
+
count.value++;
|
|
458
458
|
};
|
|
459
459
|
const setupListeners = (node: HTMLButtonElement) => {
|
|
460
460
|
// Attach the same handler multiple times with delegated: false
|
|
@@ -470,7 +470,7 @@ describe('on() event handler', () => {
|
|
|
470
470
|
};
|
|
471
471
|
<>
|
|
472
472
|
<button ref={setupListeners}>{'Click me'}</button>
|
|
473
|
-
<div class="count">{count}</div>
|
|
473
|
+
<div class="count">{count.value}</div>
|
|
474
474
|
</>
|
|
475
475
|
}
|
|
476
476
|
|
|
@@ -495,12 +495,12 @@ describe('on() event handler', () => {
|
|
|
495
495
|
|
|
496
496
|
it('should fire capture event on parent before bubbling event on child', () => {
|
|
497
497
|
function Basic() @{
|
|
498
|
-
|
|
498
|
+
const callOrder = track<string[]>([]);
|
|
499
499
|
const parentCaptureHandler = () => {
|
|
500
|
-
callOrder = [...callOrder, 'parent-capture'];
|
|
500
|
+
callOrder.value = [...callOrder.value, 'parent-capture'];
|
|
501
501
|
};
|
|
502
502
|
const childBubbleHandler = () => {
|
|
503
|
-
callOrder = [...callOrder, 'child-bubble'];
|
|
503
|
+
callOrder.value = [...callOrder.value, 'child-bubble'];
|
|
504
504
|
};
|
|
505
505
|
const setupParent = (node: HTMLDivElement) => {
|
|
506
506
|
return on(node, 'clickCapture', parentCaptureHandler);
|
|
@@ -512,7 +512,7 @@ describe('on() event handler', () => {
|
|
|
512
512
|
<div ref={setupParent} class="parent">
|
|
513
513
|
<button ref={setupChild} class="child">{'Click me'}</button>
|
|
514
514
|
</div>
|
|
515
|
-
<div class="order">{callOrder.join(',')}</div>
|
|
515
|
+
<div class="order">{callOrder.value.join(',')}</div>
|
|
516
516
|
</>
|
|
517
517
|
}
|
|
518
518
|
|
|
@@ -537,15 +537,15 @@ describe('on() event handler', () => {
|
|
|
537
537
|
|
|
538
538
|
it('should fire handler only once when once option is true', () => {
|
|
539
539
|
function Basic() @{
|
|
540
|
-
|
|
541
|
-
|
|
540
|
+
const count = track(0);
|
|
541
|
+
const permanentCount = track(0);
|
|
542
542
|
const setupListeners = (node: HTMLButtonElement) => {
|
|
543
543
|
const onceHandler = on(node, 'click', () => {
|
|
544
|
-
count++;
|
|
544
|
+
count.value++;
|
|
545
545
|
}, { once: true });
|
|
546
546
|
|
|
547
547
|
const permanentHandler = on(node, 'click', () => {
|
|
548
|
-
permanentCount++;
|
|
548
|
+
permanentCount.value++;
|
|
549
549
|
});
|
|
550
550
|
|
|
551
551
|
return () => {
|
|
@@ -555,8 +555,8 @@ describe('on() event handler', () => {
|
|
|
555
555
|
};
|
|
556
556
|
<>
|
|
557
557
|
<button ref={setupListeners}>{'Click me'}</button>
|
|
558
|
-
<div class="once-count">{count}</div>
|
|
559
|
-
<div class="permanent-count">{permanentCount}</div>
|
|
558
|
+
<div class="once-count">{count.value}</div>
|
|
559
|
+
<div class="permanent-count">{permanentCount.value}</div>
|
|
560
560
|
</>
|
|
561
561
|
}
|
|
562
562
|
|
|
@@ -591,16 +591,16 @@ describe('on() event handler', () => {
|
|
|
591
591
|
|
|
592
592
|
it('should handle click events on window', () => {
|
|
593
593
|
function Basic() @{
|
|
594
|
-
|
|
594
|
+
const windowClickCount = track(0);
|
|
595
595
|
effect(() => {
|
|
596
596
|
const removeWindowListener = on(window, 'click', () => {
|
|
597
|
-
windowClickCount++;
|
|
597
|
+
windowClickCount.value++;
|
|
598
598
|
});
|
|
599
599
|
return removeWindowListener;
|
|
600
600
|
});
|
|
601
601
|
<div>
|
|
602
602
|
<button class="test-btn">{'Click me'}</button>
|
|
603
|
-
<div class="window-count">{windowClickCount}</div>
|
|
603
|
+
<div class="window-count">{windowClickCount.value}</div>
|
|
604
604
|
</div>
|
|
605
605
|
}
|
|
606
606
|
|
|
@@ -625,16 +625,16 @@ describe('on() event handler', () => {
|
|
|
625
625
|
|
|
626
626
|
it('should handle click events on document', () => {
|
|
627
627
|
function Basic() @{
|
|
628
|
-
|
|
628
|
+
const documentClickCount = track(0);
|
|
629
629
|
effect(() => {
|
|
630
630
|
const removeDocumentListener = on(document, 'click', () => {
|
|
631
|
-
documentClickCount++;
|
|
631
|
+
documentClickCount.value++;
|
|
632
632
|
});
|
|
633
633
|
return removeDocumentListener;
|
|
634
634
|
});
|
|
635
635
|
<div>
|
|
636
636
|
<button class="test-btn">{'Click me'}</button>
|
|
637
|
-
<div class="document-count">{documentClickCount}</div>
|
|
637
|
+
<div class="document-count">{documentClickCount.value}</div>
|
|
638
638
|
</div>
|
|
639
639
|
}
|
|
640
640
|
|
|
@@ -659,16 +659,16 @@ describe('on() event handler', () => {
|
|
|
659
659
|
|
|
660
660
|
it('should handle click events on body', () => {
|
|
661
661
|
function Basic() @{
|
|
662
|
-
|
|
662
|
+
const bodyClickCount = track(0);
|
|
663
663
|
effect(() => {
|
|
664
664
|
const removeBodyListener = on(document.body, 'click', () => {
|
|
665
|
-
bodyClickCount++;
|
|
665
|
+
bodyClickCount.value++;
|
|
666
666
|
});
|
|
667
667
|
return removeBodyListener;
|
|
668
668
|
});
|
|
669
669
|
<div>
|
|
670
670
|
<button class="test-btn">{'Click me'}</button>
|
|
671
|
-
<div class="body-count">{bodyClickCount}</div>
|
|
671
|
+
<div class="body-count">{bodyClickCount.value}</div>
|
|
672
672
|
</div>
|
|
673
673
|
}
|
|
674
674
|
|
|
@@ -691,3 +691,29 @@ describe('on() event handler', () => {
|
|
|
691
691
|
expect(bodyCountDiv.textContent).toBe('2');
|
|
692
692
|
});
|
|
693
693
|
});
|
|
694
|
+
|
|
695
|
+
describe('root event delegation lifecycle', () => {
|
|
696
|
+
it('ignores a repeated unmount() so a later mount on the same target keeps delegation', () => {
|
|
697
|
+
// The target's delegated listeners are ref-counted per mount/portal. A
|
|
698
|
+
// disposer called twice (remount, HMR, a stale reference) must not
|
|
699
|
+
// release the count a fresh mount now owns.
|
|
700
|
+
const target = document.createElement('div');
|
|
701
|
+
|
|
702
|
+
function Counter() @{
|
|
703
|
+
const count = track(0);
|
|
704
|
+
<button onClick={() => count.value++}>{count.value}</button>
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const unmount_first = mount(Counter, { target });
|
|
708
|
+
unmount_first();
|
|
709
|
+
|
|
710
|
+
const unmount_second = mount(Counter, { target });
|
|
711
|
+
unmount_first();
|
|
712
|
+
|
|
713
|
+
target.querySelector('button').click();
|
|
714
|
+
flushSync();
|
|
715
|
+
expect(target.querySelector('button').textContent).toBe('1');
|
|
716
|
+
|
|
717
|
+
unmount_second();
|
|
718
|
+
});
|
|
719
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { flushSync, track, type Tracked } from 'ripple';
|
|
2
|
+
|
|
3
|
+
interface Row {
|
|
4
|
+
id: number;
|
|
5
|
+
group: string;
|
|
6
|
+
label: Tracked<string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type RowSpec = [id: number, group?: string];
|
|
10
|
+
|
|
11
|
+
interface Actions {
|
|
12
|
+
select(id: number | undefined): void;
|
|
13
|
+
setItems(rows: RowSpec[]): void;
|
|
14
|
+
setGroup(group: string | undefined): void;
|
|
15
|
+
relabel(index: number, label: string): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function selected_ids(): number[] {
|
|
19
|
+
return Array.from(container.querySelectorAll('li.selected')).map(
|
|
20
|
+
(el) => Number(el.getAttribute('data-id')),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function matched_ids(): string[] {
|
|
25
|
+
return Array.from(container.querySelectorAll('[data-group="match"]')).map(
|
|
26
|
+
(el) => el.getAttribute('data-id') ?? '',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function texts(): string[] {
|
|
31
|
+
return Array.from(container.querySelectorAll('li')).map((el) => el.textContent ?? '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('@for selector comparisons', () => {
|
|
35
|
+
let actions: Actions;
|
|
36
|
+
|
|
37
|
+
function App() @{
|
|
38
|
+
const row = (id: number, group = 'a'): Row => ({ id, group, label: track(`row ${id}`) });
|
|
39
|
+
|
|
40
|
+
const items = track<Row[]>([row(1), row(2), row(3)]);
|
|
41
|
+
const selected = track<number | undefined>(undefined);
|
|
42
|
+
const group = track<string | undefined>(undefined);
|
|
43
|
+
|
|
44
|
+
actions = {
|
|
45
|
+
select: (id) => {
|
|
46
|
+
selected.value = id;
|
|
47
|
+
},
|
|
48
|
+
setItems: (specs) => {
|
|
49
|
+
items.value = specs.map(([id, group]) => row(id, group));
|
|
50
|
+
},
|
|
51
|
+
setGroup: (next) => {
|
|
52
|
+
group.value = next;
|
|
53
|
+
},
|
|
54
|
+
relabel: (index, label) => {
|
|
55
|
+
items.value[index].label.value = label;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
<ul>
|
|
60
|
+
@for (const item of items.value; key item.id) {
|
|
61
|
+
<li
|
|
62
|
+
data-id={item.id}
|
|
63
|
+
class={selected.value === item.id ? 'selected' : ''}
|
|
64
|
+
data-group={group.value === item.group ? 'match' : ''}
|
|
65
|
+
>
|
|
66
|
+
{selected.value !== item.id ? item.label.value : 'current'}
|
|
67
|
+
</li>
|
|
68
|
+
}
|
|
69
|
+
</ul>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
it('moves the selection between rows', () => {
|
|
73
|
+
render(App);
|
|
74
|
+
expect(selected_ids()).toEqual([]);
|
|
75
|
+
|
|
76
|
+
actions.select(2);
|
|
77
|
+
flushSync();
|
|
78
|
+
expect(selected_ids()).toEqual([2]);
|
|
79
|
+
expect(texts()).toEqual(['row 1', 'current', 'row 3']);
|
|
80
|
+
|
|
81
|
+
actions.select(3);
|
|
82
|
+
flushSync();
|
|
83
|
+
expect(selected_ids()).toEqual([3]);
|
|
84
|
+
expect(texts()).toEqual(['row 1', 'row 2', 'current']);
|
|
85
|
+
|
|
86
|
+
actions.select(undefined);
|
|
87
|
+
flushSync();
|
|
88
|
+
expect(selected_ids()).toEqual([]);
|
|
89
|
+
expect(texts()).toEqual(['row 1', 'row 2', 'row 3']);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('matches rows created after the selection was set', () => {
|
|
93
|
+
render(App);
|
|
94
|
+
|
|
95
|
+
actions.select(5);
|
|
96
|
+
flushSync();
|
|
97
|
+
expect(selected_ids()).toEqual([]);
|
|
98
|
+
|
|
99
|
+
actions.setItems([[1], [5]]);
|
|
100
|
+
flushSync();
|
|
101
|
+
expect(selected_ids()).toEqual([5]);
|
|
102
|
+
|
|
103
|
+
actions.setItems([[1]]);
|
|
104
|
+
flushSync();
|
|
105
|
+
expect(selected_ids()).toEqual([]);
|
|
106
|
+
|
|
107
|
+
actions.setItems([[1], [5]]);
|
|
108
|
+
flushSync();
|
|
109
|
+
expect(selected_ids()).toEqual([5]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('re-registers a row whose compared value changes', () => {
|
|
113
|
+
render(App);
|
|
114
|
+
|
|
115
|
+
actions.setItems([
|
|
116
|
+
[1, 'a'],
|
|
117
|
+
[2, 'b'],
|
|
118
|
+
]);
|
|
119
|
+
actions.setGroup('b');
|
|
120
|
+
flushSync();
|
|
121
|
+
expect(matched_ids()).toEqual(['2']);
|
|
122
|
+
|
|
123
|
+
// Same keys, so the existing rows update in place with new groups.
|
|
124
|
+
actions.setItems([
|
|
125
|
+
[1, 'b'],
|
|
126
|
+
[2, 'a'],
|
|
127
|
+
]);
|
|
128
|
+
flushSync();
|
|
129
|
+
expect(matched_ids()).toEqual(['1']);
|
|
130
|
+
|
|
131
|
+
actions.setGroup('a');
|
|
132
|
+
flushSync();
|
|
133
|
+
expect(matched_ids()).toEqual(['2']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('updates every row sharing the compared value', () => {
|
|
137
|
+
render(App);
|
|
138
|
+
|
|
139
|
+
actions.setItems([
|
|
140
|
+
[1, 'a'],
|
|
141
|
+
[2, 'b'],
|
|
142
|
+
[3, 'a'],
|
|
143
|
+
]);
|
|
144
|
+
actions.setGroup('a');
|
|
145
|
+
flushSync();
|
|
146
|
+
expect(matched_ids()).toEqual(['1', '3']);
|
|
147
|
+
|
|
148
|
+
actions.setGroup('b');
|
|
149
|
+
flushSync();
|
|
150
|
+
expect(matched_ids()).toEqual(['2']);
|
|
151
|
+
|
|
152
|
+
actions.setGroup(undefined);
|
|
153
|
+
flushSync();
|
|
154
|
+
expect(matched_ids()).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('keeps the rest of the row expression reactive', () => {
|
|
158
|
+
render(App);
|
|
159
|
+
|
|
160
|
+
actions.select(1);
|
|
161
|
+
flushSync();
|
|
162
|
+
expect(texts()).toEqual(['current', 'row 2', 'row 3']);
|
|
163
|
+
|
|
164
|
+
actions.relabel(1, 'second');
|
|
165
|
+
flushSync();
|
|
166
|
+
expect(texts()).toEqual(['current', 'second', 'row 3']);
|
|
167
|
+
expect(selected_ids()).toEqual([1]);
|
|
168
|
+
|
|
169
|
+
actions.select(2);
|
|
170
|
+
flushSync();
|
|
171
|
+
expect(texts()).toEqual(['row 1', 'current', 'row 3']);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('keyed reconciliation with most items moving', () => {
|
|
176
|
+
it('keeps node identity and order when a large list is shuffled', () => {
|
|
177
|
+
let apply: (order: number[]) => void = () => {};
|
|
178
|
+
|
|
179
|
+
function App() @{
|
|
180
|
+
const items = track(Array.from({ length: 300 }, (_, i) => i));
|
|
181
|
+
apply = (order) => {
|
|
182
|
+
items.value = order;
|
|
183
|
+
};
|
|
184
|
+
<ul>
|
|
185
|
+
@for (const item of items.value; key item) {
|
|
186
|
+
<li data-id={item}>{item}</li>
|
|
187
|
+
}
|
|
188
|
+
</ul>
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
render(App);
|
|
192
|
+
const before = new Map(Array.from(
|
|
193
|
+
container.querySelectorAll('li'),
|
|
194
|
+
(el) => [el.getAttribute('data-id'), el],
|
|
195
|
+
));
|
|
196
|
+
|
|
197
|
+
// A permutation that moves nearly every item, plus a few new ones.
|
|
198
|
+
let seed = 7;
|
|
199
|
+
const next = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
|
|
200
|
+
const order = Array.from({ length: 300 }, (_, i) => i);
|
|
201
|
+
for (let i = order.length - 1; i > 0; i--) {
|
|
202
|
+
const j = Math.floor(next() * (i + 1));
|
|
203
|
+
[order[i], order[j]] = [order[j], order[i]];
|
|
204
|
+
}
|
|
205
|
+
order.splice(10, 0, 1000);
|
|
206
|
+
order.splice(200, 0, 1001);
|
|
207
|
+
|
|
208
|
+
apply(order);
|
|
209
|
+
flushSync();
|
|
210
|
+
|
|
211
|
+
const after = Array.from(container.querySelectorAll('li'));
|
|
212
|
+
expect(after.map((el) => Number(el.getAttribute('data-id')))).toEqual(order);
|
|
213
|
+
for (const el of after) {
|
|
214
|
+
const id = el.getAttribute('data-id');
|
|
215
|
+
if (before.has(id)) {
|
|
216
|
+
expect(el).toBe(before.get(id));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
expect(after.map((el) => el.textContent)).toEqual(order.map(String));
|
|
220
|
+
});
|
|
221
|
+
});
|