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
|
@@ -31,18 +31,18 @@ describe('head elements', () => {
|
|
|
31
31
|
|
|
32
32
|
it('renders reactive title element', () => {
|
|
33
33
|
function App() @{
|
|
34
|
-
|
|
34
|
+
const title = track('Initial Title');
|
|
35
35
|
<>
|
|
36
36
|
<head>
|
|
37
|
-
<title>{title}</title>
|
|
37
|
+
<title>{title.value}</title>
|
|
38
38
|
</head>
|
|
39
39
|
<div>
|
|
40
40
|
<button
|
|
41
41
|
onClick={() => {
|
|
42
|
-
title = 'Updated Title';
|
|
42
|
+
title.value = 'Updated Title';
|
|
43
43
|
}}
|
|
44
44
|
>{'Update Title'}</button>
|
|
45
|
-
<span>{title}</span>
|
|
45
|
+
<span>{title.value}</span>
|
|
46
46
|
</div>
|
|
47
47
|
</>
|
|
48
48
|
}
|
|
@@ -62,15 +62,15 @@ describe('head elements', () => {
|
|
|
62
62
|
|
|
63
63
|
it('renders title with template literal', () => {
|
|
64
64
|
function App() @{
|
|
65
|
-
|
|
65
|
+
const name = track('World');
|
|
66
66
|
<>
|
|
67
67
|
<head>
|
|
68
|
-
<title>{`Hello ${name}!`}</title>
|
|
68
|
+
<title>{`Hello ${name.value}!`}</title>
|
|
69
69
|
</head>
|
|
70
70
|
<div>
|
|
71
71
|
<button
|
|
72
72
|
onClick={() => {
|
|
73
|
-
name = 'Ripple';
|
|
73
|
+
name.value = 'Ripple';
|
|
74
74
|
}}
|
|
75
75
|
>{'Change Name'}</button>
|
|
76
76
|
</div>
|
|
@@ -90,19 +90,19 @@ describe('head elements', () => {
|
|
|
90
90
|
|
|
91
91
|
it('renders title with computed value', () => {
|
|
92
92
|
function App() @{
|
|
93
|
-
|
|
93
|
+
const count = track(0);
|
|
94
94
|
let prefix = 'Count: ';
|
|
95
95
|
<>
|
|
96
96
|
<head>
|
|
97
|
-
<title>{prefix + count}</title>
|
|
97
|
+
<title>{prefix + count.value}</title>
|
|
98
98
|
</head>
|
|
99
99
|
<div>
|
|
100
100
|
<button
|
|
101
101
|
onClick={() => {
|
|
102
|
-
count++;
|
|
102
|
+
count.value++;
|
|
103
103
|
}}
|
|
104
104
|
>{'Increment'}</button>
|
|
105
|
-
<span>{count}</span>
|
|
105
|
+
<span>{count.value}</span>
|
|
106
106
|
</div>
|
|
107
107
|
</>
|
|
108
108
|
}
|
|
@@ -121,15 +121,15 @@ describe('head elements', () => {
|
|
|
121
121
|
|
|
122
122
|
it('handles multiple title updates', () => {
|
|
123
123
|
function App() @{
|
|
124
|
-
|
|
124
|
+
const step = track(1);
|
|
125
125
|
<>
|
|
126
126
|
<head>
|
|
127
|
-
<title>{`Step ${step} of 3`}</title>
|
|
127
|
+
<title>{`Step ${step.value} of 3`}</title>
|
|
128
128
|
</head>
|
|
129
129
|
<div>
|
|
130
130
|
<button
|
|
131
131
|
onClick={() => {
|
|
132
|
-
step = step % 3 + 1;
|
|
132
|
+
step.value = step.value % 3 + 1;
|
|
133
133
|
}}
|
|
134
134
|
>{'Next Step'}</button>
|
|
135
135
|
</div>
|
|
@@ -244,23 +244,23 @@ describe('head elements', () => {
|
|
|
244
244
|
|
|
245
245
|
it('renders title with conditional content', () => {
|
|
246
246
|
function App() @{
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
const showPrefix = track(true);
|
|
248
|
+
const title = track('Main Page');
|
|
249
249
|
<>
|
|
250
250
|
<head>
|
|
251
251
|
<title>
|
|
252
|
-
{showPrefix ? 'App - ' + title : title}
|
|
252
|
+
{showPrefix.value ? 'App - ' + title.value : title.value}
|
|
253
253
|
</title>
|
|
254
254
|
</head>
|
|
255
255
|
<div>
|
|
256
256
|
<button
|
|
257
257
|
onClick={() => {
|
|
258
|
-
showPrefix = !showPrefix;
|
|
258
|
+
showPrefix.value = !showPrefix.value;
|
|
259
259
|
}}
|
|
260
260
|
>{'Toggle Prefix'}</button>
|
|
261
261
|
<button
|
|
262
262
|
onClick={() => {
|
|
263
|
-
title = title === 'Main Page' ? 'Settings' : 'Main Page';
|
|
263
|
+
title.value = title.value === 'Main Page' ? 'Settings' : 'Main Page';
|
|
264
264
|
}}
|
|
265
265
|
>{'Change Page'}</button>
|
|
266
266
|
</div>
|
|
@@ -16,12 +16,14 @@ describe('Fragment innerHTML', () => {
|
|
|
16
16
|
|
|
17
17
|
it('renders spread innerHTML as content instead of an attribute', () => {
|
|
18
18
|
function App() @{
|
|
19
|
-
|
|
19
|
+
const attrs = track<{ innerHTML?: string }>({
|
|
20
20
|
innerHTML: '<span>Spread HTML</span>',
|
|
21
21
|
});
|
|
22
22
|
<>
|
|
23
|
-
<code {...attrs} />
|
|
24
|
-
<button
|
|
23
|
+
<code {...attrs.value} />
|
|
24
|
+
<button
|
|
25
|
+
onClick={() => (attrs.value = { innerHTML: '<em>Updated HTML</em>' })}
|
|
26
|
+
>{'Update'}</button>
|
|
25
27
|
</>
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -64,12 +66,12 @@ describe('Fragment innerHTML', () => {
|
|
|
64
66
|
|
|
65
67
|
it('renders dynamic innerHTML fragments', () => {
|
|
66
68
|
function App() @{
|
|
67
|
-
|
|
69
|
+
const str = track('<div>Test</div>');
|
|
68
70
|
<>
|
|
69
|
-
<Fragment innerHTML={str} />
|
|
71
|
+
<Fragment innerHTML={str.value} />
|
|
70
72
|
<button
|
|
71
73
|
onClick={() => {
|
|
72
|
-
str = '<div>Updated</div>';
|
|
74
|
+
str.value = '<div>Updated</div>';
|
|
73
75
|
}}
|
|
74
76
|
>{'Update'}</button>
|
|
75
77
|
</>
|
|
@@ -99,10 +101,10 @@ describe('Fragment innerHTML', () => {
|
|
|
99
101
|
|
|
100
102
|
it('updates dynamic Fragment innerHTML', () => {
|
|
101
103
|
function App() @{
|
|
102
|
-
|
|
104
|
+
const html = track('<span>First</span>');
|
|
103
105
|
<>
|
|
104
|
-
<Fragment innerHTML={html} />
|
|
105
|
-
<button onClick={() => (html = '<em>Second</em>')}>{'Update'}</button>
|
|
106
|
+
<Fragment innerHTML={html.value} />
|
|
107
|
+
<button onClick={() => (html.value = '<em>Second</em>')}>{'Update'}</button>
|
|
106
108
|
</>
|
|
107
109
|
}
|
|
108
110
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { flushSync, track, type Tracked } from 'ripple';
|
|
3
|
+
|
|
4
|
+
// `if_block` probes its condition once: a condition that reads no tracked
|
|
5
|
+
// state renders its branch with no block, a dynamic one gets a block whose
|
|
6
|
+
// own first run subscribes it. The dependencies the probe recorded are never
|
|
7
|
+
// handed to the block: a write during the branch's first render (a child's
|
|
8
|
+
// setup here) would prune them before the block could take them over.
|
|
9
|
+
|
|
10
|
+
describe('if block', () => {
|
|
11
|
+
it('keeps re-evaluating when the first branch render writes the condition state', () => {
|
|
12
|
+
let set: (value: number) => void = () => {};
|
|
13
|
+
|
|
14
|
+
function Bumper(props: { n: Tracked<number> }) @{
|
|
15
|
+
// Writes the value the parent's condition depends on, while the
|
|
16
|
+
// parent's if is rendering this branch for the first time.
|
|
17
|
+
props.n.value = props.n.value + 1;
|
|
18
|
+
<span>{'bumped ' + props.n.value}</span>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function App() @{
|
|
22
|
+
const n = track(0);
|
|
23
|
+
set = (value) => {
|
|
24
|
+
n.value = value;
|
|
25
|
+
};
|
|
26
|
+
@if (n.value < 5) {
|
|
27
|
+
<Bumper {n} />
|
|
28
|
+
} @else {
|
|
29
|
+
<p>{'done'}</p>
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
render(App);
|
|
34
|
+
flushSync();
|
|
35
|
+
expect(container.querySelector('span')?.textContent).toBe('bumped 1');
|
|
36
|
+
|
|
37
|
+
set(7);
|
|
38
|
+
flushSync();
|
|
39
|
+
expect(container.querySelector('span')).toBeNull();
|
|
40
|
+
expect(container.querySelector('p')?.textContent).toBe('done');
|
|
41
|
+
|
|
42
|
+
set(0);
|
|
43
|
+
flushSync();
|
|
44
|
+
expect(container.querySelector('span')?.textContent).toBe('bumped 1');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders a static condition with no block and keeps a dynamic one live', () => {
|
|
48
|
+
let toggle: () => void = () => {};
|
|
49
|
+
|
|
50
|
+
function App(props: { fixed: boolean }) @{
|
|
51
|
+
const on = track(false);
|
|
52
|
+
toggle = () => {
|
|
53
|
+
on.value = !on.value;
|
|
54
|
+
};
|
|
55
|
+
<>
|
|
56
|
+
@if (props.fixed) {
|
|
57
|
+
<b>{'fixed'}</b>
|
|
58
|
+
}
|
|
59
|
+
@if (on.value) {
|
|
60
|
+
<i>{'on'}</i>
|
|
61
|
+
} @else {
|
|
62
|
+
<i>{'off'}</i>
|
|
63
|
+
}
|
|
64
|
+
</>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Root() @{
|
|
68
|
+
<App fixed={true} />
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
render(Root);
|
|
72
|
+
expect(container.querySelector('b')?.textContent).toBe('fixed');
|
|
73
|
+
expect(container.querySelector('i')?.textContent).toBe('off');
|
|
74
|
+
|
|
75
|
+
toggle();
|
|
76
|
+
flushSync();
|
|
77
|
+
expect(container.querySelector('i')?.textContent).toBe('on');
|
|
78
|
+
expect(container.querySelector('b')?.textContent).toBe('fixed');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -4,15 +4,15 @@ describe('RippleMap', () => {
|
|
|
4
4
|
it('handles set with update and delete operations with a reactive size var', () => {
|
|
5
5
|
function MapTest() @{
|
|
6
6
|
let map = new RippleMap([['a', 1], ['b', 2], ['c', 3]]);
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const value = track(() => map.get('a'));
|
|
8
|
+
const size = track(() => map.size);
|
|
9
9
|
<>
|
|
10
10
|
<button onClick={() => map.set('d', 4)}>{'set'}</button>
|
|
11
11
|
<button onClick={() => map.delete('b')}>{'delete'}</button>
|
|
12
12
|
<button onClick={() => map.set('a', 5)}>{'update'}</button>
|
|
13
13
|
<pre>{map.get('d')}</pre>
|
|
14
|
-
<pre>{size}</pre>
|
|
15
|
-
<pre>{value}</pre>
|
|
14
|
+
<pre>{size.value}</pre>
|
|
15
|
+
<pre>{value.value}</pre>
|
|
16
16
|
</>
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -62,10 +62,10 @@ describe('RippleMap', () => {
|
|
|
62
62
|
it('handles has operation and tracks reactive $has', () => {
|
|
63
63
|
function MapTest() @{
|
|
64
64
|
let map = new RippleMap([['a', 1], ['b', 2], ['c', 3]]);
|
|
65
|
-
|
|
65
|
+
const has = track(() => map.has('b'));
|
|
66
66
|
<>
|
|
67
67
|
<button onClick={() => map.delete('b')}>{'delete'}</button>
|
|
68
|
-
<pre>{has}</pre>
|
|
68
|
+
<pre>{has.value}</pre>
|
|
69
69
|
</>
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -83,14 +83,14 @@ describe('RippleMap', () => {
|
|
|
83
83
|
it('handles reactivity of keys, values, and entries', () => {
|
|
84
84
|
function MapTest() @{
|
|
85
85
|
let map = new RippleMap([['x', 10], ['y', 20]]);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
const keys = track(() => Array.from(map.keys()));
|
|
87
|
+
const values = track(() => Array.from(map.values()));
|
|
88
|
+
const entries = track(() => Array.from(map.entries()));
|
|
89
89
|
<>
|
|
90
90
|
<button onClick={() => map.delete('x')}>{'delete'}</button>
|
|
91
|
-
<pre>{JSON.stringify(keys)}</pre>
|
|
92
|
-
<pre>{JSON.stringify(values)}</pre>
|
|
93
|
-
<pre>{JSON.stringify(entries)}</pre>
|
|
91
|
+
<pre>{JSON.stringify(keys.value)}</pre>
|
|
92
|
+
<pre>{JSON.stringify(values.value)}</pre>
|
|
93
|
+
<pre>{JSON.stringify(entries.value)}</pre>
|
|
94
94
|
</>
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -144,11 +144,11 @@ describe('RippleMap', () => {
|
|
|
144
144
|
it('creates RippleMap with initial entries using RippleMap() shorthand syntax', () => {
|
|
145
145
|
function MapTest() @{
|
|
146
146
|
let map = new RippleMap([['a', 1], ['b', 2], ['c', 3]]);
|
|
147
|
-
|
|
147
|
+
const value = track(() => map.get('b'));
|
|
148
148
|
<>
|
|
149
149
|
<button onClick={() => map.set('b', 10)}>{'update'}</button>
|
|
150
150
|
<pre>{map.size}</pre>
|
|
151
|
-
<pre>{value}</pre>
|
|
151
|
+
<pre>{value.value}</pre>
|
|
152
152
|
</>
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -167,12 +167,12 @@ describe('RippleMap', () => {
|
|
|
167
167
|
it('handles all operations with RippleMap() shorthand syntax', () => {
|
|
168
168
|
function MapTest() @{
|
|
169
169
|
let map = new RippleMap([['x', 100], ['y', 200]]);
|
|
170
|
-
|
|
170
|
+
const keys = track(() => Array.from(map.keys()));
|
|
171
171
|
<>
|
|
172
172
|
<button onClick={() => map.set('z', 300)}>{'add'}</button>
|
|
173
173
|
<button onClick={() => map.delete('x')}>{'delete'}</button>
|
|
174
174
|
<button onClick={() => map.clear()}>{'clear'}</button>
|
|
175
|
-
<pre>{JSON.stringify(keys)}</pre>
|
|
175
|
+
<pre>{JSON.stringify(keys.value)}</pre>
|
|
176
176
|
<pre>{map.size}</pre>
|
|
177
177
|
</>
|
|
178
178
|
}
|
|
@@ -51,9 +51,9 @@ describe('MediaQuery', () => {
|
|
|
51
51
|
const media = '(min-width: 600px)';
|
|
52
52
|
|
|
53
53
|
function App() @{
|
|
54
|
-
|
|
54
|
+
const medium = new MediaQuery(media);
|
|
55
55
|
<div>
|
|
56
|
-
<p>{medium}</p>
|
|
56
|
+
<p>{medium.value}</p>
|
|
57
57
|
</div>
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -85,19 +85,19 @@ describe('MediaQuery', () => {
|
|
|
85
85
|
const media = '(min-width: 600px)';
|
|
86
86
|
|
|
87
87
|
function App() @{
|
|
88
|
-
|
|
88
|
+
const show = track(true);
|
|
89
89
|
<>
|
|
90
|
-
@if (show) {
|
|
90
|
+
@if (show.value) {
|
|
91
91
|
<Child />
|
|
92
92
|
}
|
|
93
|
-
<button onClick={() => (show = !show)}>{'Toggle Child'}</button>
|
|
93
|
+
<button onClick={() => (show.value = !show.value)}>{'Toggle Child'}</button>
|
|
94
94
|
</>
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
function Child() @{
|
|
98
|
-
|
|
98
|
+
const medium = new MediaQuery(media);
|
|
99
99
|
<div>
|
|
100
|
-
<p>{medium}</p>
|
|
100
|
+
<p>{medium.value}</p>
|
|
101
101
|
</div>
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Portal, flushSync, track } from 'ripple';
|
|
1
|
+
import { Portal, flushSync, track, type Tracked } from 'ripple';
|
|
2
2
|
|
|
3
3
|
describe('Portal', () => {
|
|
4
4
|
afterEach(() => {
|
|
@@ -46,14 +46,14 @@ describe('Portal', () => {
|
|
|
46
46
|
|
|
47
47
|
it('cleans up portal content when destroyed via conditional rendering', () => {
|
|
48
48
|
function TestPortal() @{
|
|
49
|
-
|
|
49
|
+
const open = track(true);
|
|
50
50
|
<>
|
|
51
|
-
@if (open) {
|
|
51
|
+
@if (open.value) {
|
|
52
52
|
<Portal target={document.body}>
|
|
53
53
|
<div class="test-portal">{'Conditional content'}</div>
|
|
54
54
|
</Portal>
|
|
55
55
|
}
|
|
56
|
-
<button onClick={() => (open = false)}>{'Close'}</button>
|
|
56
|
+
<button onClick={() => (open.value = false)}>{'Close'}</button>
|
|
57
57
|
</>
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -72,18 +72,18 @@ describe('Portal', () => {
|
|
|
72
72
|
|
|
73
73
|
it('opens and closes portal via conditional rendering', () => {
|
|
74
74
|
function TestPortal() @{
|
|
75
|
-
|
|
75
|
+
const open = track(false);
|
|
76
76
|
<>
|
|
77
|
-
@if (open) {
|
|
77
|
+
@if (open.value) {
|
|
78
78
|
<Portal target={document.body}>
|
|
79
79
|
<div class="test-portal">
|
|
80
80
|
{'Content'}
|
|
81
|
-
<button onClick={() => (open = false)}>{'Close'}</button>
|
|
81
|
+
<button onClick={() => (open.value = false)}>{'Close'}</button>
|
|
82
82
|
</div>
|
|
83
83
|
</Portal>
|
|
84
84
|
}
|
|
85
|
-
@if (!open) {
|
|
86
|
-
<button onClick={() => (open = true)}>{'Open'}</button>
|
|
85
|
+
@if (!open.value) {
|
|
86
|
+
<button onClick={() => (open.value = true)}>{'Open'}</button>
|
|
87
87
|
}
|
|
88
88
|
</>
|
|
89
89
|
}
|
|
@@ -146,28 +146,28 @@ describe('Portal', () => {
|
|
|
146
146
|
// down the shared delegated event listeners for *all* portals on that
|
|
147
147
|
// target, silently breaking clicks inside any portal still open.
|
|
148
148
|
function TestSharedTargetPortals() @{
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
const modalOpen = track(true);
|
|
150
|
+
const sheetOpen = track(true);
|
|
151
|
+
const sheetClicks = track(0);
|
|
152
152
|
<>
|
|
153
|
-
@if (modalOpen) {
|
|
153
|
+
@if (modalOpen.value) {
|
|
154
154
|
<Portal target={document.body}>
|
|
155
155
|
<div class="test-portal test-modal">
|
|
156
156
|
<button
|
|
157
157
|
class="close-modal"
|
|
158
|
-
onClick={() => (modalOpen = false)}
|
|
158
|
+
onClick={() => (modalOpen.value = false)}
|
|
159
159
|
>{'Close modal'}</button>
|
|
160
160
|
</div>
|
|
161
161
|
</Portal>
|
|
162
162
|
}
|
|
163
|
-
@if (sheetOpen) {
|
|
163
|
+
@if (sheetOpen.value) {
|
|
164
164
|
<Portal target={document.body}>
|
|
165
165
|
<div class="test-portal test-sheet">
|
|
166
|
-
<span class="sheet-clicks">{String(sheetClicks)}</span>
|
|
166
|
+
<span class="sheet-clicks">{String(sheetClicks.value)}</span>
|
|
167
167
|
<button
|
|
168
168
|
class="sheet-btn"
|
|
169
169
|
onClick={() => {
|
|
170
|
-
sheetClicks++;
|
|
170
|
+
sheetClicks.value++;
|
|
171
171
|
}}
|
|
172
172
|
>{'Sheet button'}</button>
|
|
173
173
|
</div>
|
|
@@ -209,14 +209,14 @@ describe('Portal', () => {
|
|
|
209
209
|
document.body.appendChild(target2);
|
|
210
210
|
|
|
211
211
|
function TestRetargetPortal() @{
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
const useSecond = track(false);
|
|
213
|
+
const clicks = track(0);
|
|
214
214
|
<>
|
|
215
|
-
<button class="swap" onClick={() => (useSecond = true)}>{'Swap'}</button>
|
|
216
|
-
<Portal target={useSecond ? target2 : target1}>
|
|
215
|
+
<button class="swap" onClick={() => (useSecond.value = true)}>{'Swap'}</button>
|
|
216
|
+
<Portal target={useSecond.value ? target2 : target1}>
|
|
217
217
|
<div class="test-portal">
|
|
218
|
-
<span class="clicks">{String(clicks)}</span>
|
|
219
|
-
<button class="portal-btn" onClick={() => clicks++}>{'Click'}</button>
|
|
218
|
+
<span class="clicks">{String(clicks.value)}</span>
|
|
219
|
+
<button class="portal-btn" onClick={() => clicks.value++}>{'Click'}</button>
|
|
220
220
|
</div>
|
|
221
221
|
</Portal>
|
|
222
222
|
</>
|
|
@@ -246,12 +246,12 @@ describe('Portal', () => {
|
|
|
246
246
|
|
|
247
247
|
it('handles portal with reactive content', () => {
|
|
248
248
|
function TestReactivePortal() @{
|
|
249
|
-
|
|
249
|
+
const count = track(0);
|
|
250
250
|
<Portal target={document.body}>
|
|
251
251
|
<div class="test-portal">
|
|
252
252
|
{'Count: '}
|
|
253
|
-
{String(count)}
|
|
254
|
-
<button onClick={() => count++}>{'Increment'}</button>
|
|
253
|
+
{String(count.value)}
|
|
254
|
+
<button onClick={() => count.value++}>{'Increment'}</button>
|
|
255
255
|
</div>
|
|
256
256
|
</Portal>
|
|
257
257
|
}
|
|
@@ -268,4 +268,202 @@ describe('Portal', () => {
|
|
|
268
268
|
|
|
269
269
|
expect(portalElement.textContent).toContain('Count: 1');
|
|
270
270
|
});
|
|
271
|
+
|
|
272
|
+
it('re-renders a root @if child at its own position among sibling portals', () => {
|
|
273
|
+
// Portal content appends through an append-into sentinel with no anchor
|
|
274
|
+
// node of its own. A child component whose root is control flow must
|
|
275
|
+
// still re-render where it was, not after content appended since.
|
|
276
|
+
const target = document.createElement('div');
|
|
277
|
+
document.body.appendChild(target);
|
|
278
|
+
|
|
279
|
+
function Toggle(props: { on: Tracked<boolean> }) @{
|
|
280
|
+
@if (props.on.value) {
|
|
281
|
+
<span class="test-portal">{'A'}</span>
|
|
282
|
+
} @else {
|
|
283
|
+
<span class="test-portal">{'a'}</span>
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function TestPortalOrder() @{
|
|
288
|
+
const on = track(true);
|
|
289
|
+
<>
|
|
290
|
+
<button onClick={() => (on.value = !on.value)}>{'Toggle'}</button>
|
|
291
|
+
<Portal {target}>
|
|
292
|
+
<Toggle {on} />
|
|
293
|
+
</Portal>
|
|
294
|
+
<Portal {target}>
|
|
295
|
+
<span class="test-portal">{'B'}</span>
|
|
296
|
+
</Portal>
|
|
297
|
+
</>
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
render(TestPortalOrder);
|
|
301
|
+
|
|
302
|
+
expect(target.textContent).toBe('AB');
|
|
303
|
+
|
|
304
|
+
container.querySelector('button').click();
|
|
305
|
+
flushSync();
|
|
306
|
+
expect(target.textContent).toBe('aB');
|
|
307
|
+
|
|
308
|
+
container.querySelector('button').click();
|
|
309
|
+
flushSync();
|
|
310
|
+
expect(target.textContent).toBe('AB');
|
|
311
|
+
|
|
312
|
+
document.body.removeChild(target);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('moves items to the end of a keyed @for rendered as a component root inside a portal', () => {
|
|
316
|
+
const target = document.createElement('div');
|
|
317
|
+
document.body.appendChild(target);
|
|
318
|
+
|
|
319
|
+
function List(props: { items: Tracked<number[]> }) @{
|
|
320
|
+
@for (const item of props.items.value; key item) {
|
|
321
|
+
<span class="test-portal">{item}</span>
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function TestPortalList() @{
|
|
326
|
+
const items = track([1, 2, 3]);
|
|
327
|
+
<>
|
|
328
|
+
<button
|
|
329
|
+
onClick={() => (items.value = [items.value[1], items.value[2], items.value[0]])}
|
|
330
|
+
>{'Rotate'}</button>
|
|
331
|
+
<Portal {target}>
|
|
332
|
+
<List {items} />
|
|
333
|
+
</Portal>
|
|
334
|
+
</>
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
render(TestPortalList);
|
|
338
|
+
|
|
339
|
+
expect(target.textContent).toBe('123');
|
|
340
|
+
|
|
341
|
+
// Moving the first item to the end inserts before the list's anchor.
|
|
342
|
+
container.querySelector('button').click();
|
|
343
|
+
flushSync();
|
|
344
|
+
expect(target.textContent).toBe('231');
|
|
345
|
+
|
|
346
|
+
document.body.removeChild(target);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('moves items to the end of a keyed @for rendered directly as portal children', () => {
|
|
350
|
+
const target = document.createElement('div');
|
|
351
|
+
document.body.appendChild(target);
|
|
352
|
+
|
|
353
|
+
function TestPortalFor() @{
|
|
354
|
+
const items = track([1, 2, 3]);
|
|
355
|
+
<>
|
|
356
|
+
<button
|
|
357
|
+
onClick={() => (items.value = [items.value[1], items.value[2], items.value[0]])}
|
|
358
|
+
>{'Rotate'}</button>
|
|
359
|
+
<Portal {target}>
|
|
360
|
+
@for (const item of items.value; key item) {
|
|
361
|
+
<span class="test-portal">{item}</span>
|
|
362
|
+
}
|
|
363
|
+
</Portal>
|
|
364
|
+
</>
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
render(TestPortalFor);
|
|
368
|
+
|
|
369
|
+
expect(target.textContent).toBe('123');
|
|
370
|
+
|
|
371
|
+
container.querySelector('button').click();
|
|
372
|
+
flushSync();
|
|
373
|
+
expect(target.textContent).toBe('231');
|
|
374
|
+
|
|
375
|
+
document.body.removeChild(target);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('removes portal content when a fragment containing the portal is destroyed', () => {
|
|
379
|
+
// The `@if` branch owns a DOM range of its own (the fragment), so it
|
|
380
|
+
// tells its subtree not to remove DOM; the portal content lives in the
|
|
381
|
+
// target, outside that range, and must still go.
|
|
382
|
+
function TestPortalInFragment() @{
|
|
383
|
+
const open = track(true);
|
|
384
|
+
<>
|
|
385
|
+
<button onClick={() => (open.value = false)}>{'Close'}</button>
|
|
386
|
+
@if (open.value) {
|
|
387
|
+
<>
|
|
388
|
+
<Portal target={document.body}>
|
|
389
|
+
<div class="test-portal">{'Content'}</div>
|
|
390
|
+
</Portal>
|
|
391
|
+
<span class="sibling">{'Sibling'}</span>
|
|
392
|
+
</>
|
|
393
|
+
}
|
|
394
|
+
</>
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
render(TestPortalInFragment);
|
|
398
|
+
|
|
399
|
+
expect(document.body.querySelector('.test-portal')).toBeTruthy();
|
|
400
|
+
|
|
401
|
+
container.querySelector('button').click();
|
|
402
|
+
flushSync();
|
|
403
|
+
|
|
404
|
+
expect(container.querySelector('.sibling')).toBeNull();
|
|
405
|
+
expect(document.body.querySelector('.test-portal')).toBeNull();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it(
|
|
409
|
+
'leaves nothing in the target after closing or retargeting a portal with control-flow roots',
|
|
410
|
+
() => {
|
|
411
|
+
// A root-controlled block inside portal content gets a text anchor of its
|
|
412
|
+
// own; the target outlives the portal, so that anchor must go with the
|
|
413
|
+
// content on close and on retarget.
|
|
414
|
+
const target1 = document.createElement('div');
|
|
415
|
+
const target2 = document.createElement('div');
|
|
416
|
+
document.body.appendChild(target1);
|
|
417
|
+
document.body.appendChild(target2);
|
|
418
|
+
|
|
419
|
+
function Toggle(props) @{
|
|
420
|
+
@if (props.on) {
|
|
421
|
+
<span class="test-portal">{'A'}</span>
|
|
422
|
+
} @else {
|
|
423
|
+
<span class="test-portal">{'a'}</span>
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function List(props) @{
|
|
428
|
+
@for (const item of props.items; key item) {
|
|
429
|
+
<span class="test-portal">{item}</span>
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function TestPortalCleanup() @{
|
|
434
|
+
const open = track(true);
|
|
435
|
+
const useSecond = track(false);
|
|
436
|
+
<>
|
|
437
|
+
<button class="close" onClick={() => (open.value = false)}>{'Close'}</button>
|
|
438
|
+
<button class="swap" onClick={() => (useSecond.value = true)}>{'Swap'}</button>
|
|
439
|
+
@if (open.value) {
|
|
440
|
+
<>
|
|
441
|
+
<Portal target={useSecond.value ? target2 : target1}>
|
|
442
|
+
<Toggle on={true} />
|
|
443
|
+
</Portal>
|
|
444
|
+
<Portal target={useSecond.value ? target2 : target1}>
|
|
445
|
+
<List items={[1, 2]} />
|
|
446
|
+
</Portal>
|
|
447
|
+
</>
|
|
448
|
+
}
|
|
449
|
+
</>
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
render(TestPortalCleanup);
|
|
453
|
+
|
|
454
|
+
expect(target1.textContent).toBe('A12');
|
|
455
|
+
|
|
456
|
+
container.querySelector('.swap').click();
|
|
457
|
+
flushSync();
|
|
458
|
+
expect(target1.childNodes.length).toBe(0);
|
|
459
|
+
expect(target2.textContent).toBe('A12');
|
|
460
|
+
|
|
461
|
+
container.querySelector('.close').click();
|
|
462
|
+
flushSync();
|
|
463
|
+
expect(target2.childNodes.length).toBe(0);
|
|
464
|
+
|
|
465
|
+
document.body.removeChild(target1);
|
|
466
|
+
document.body.removeChild(target2);
|
|
467
|
+
},
|
|
468
|
+
);
|
|
271
469
|
});
|