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,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
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { flushSync, track } from 'ripple';
|
|
2
|
+
|
|
3
|
+
it('renders shadowed intrinsic calls as elements beside text', () => {
|
|
4
|
+
function App() @{
|
|
5
|
+
const String = () => <b>string</b>;
|
|
6
|
+
const Number = () => <b>number</b>;
|
|
7
|
+
const Boolean = () => <b>boolean</b>;
|
|
8
|
+
const BigInt = () => <b>bigint</b>;
|
|
9
|
+
const Date = () => <b>date</b>;
|
|
10
|
+
<>
|
|
11
|
+
<p>before{String()}after</p>
|
|
12
|
+
<p>before{Number()}after</p>
|
|
13
|
+
<p>before{Boolean()}after</p>
|
|
14
|
+
<p>before{BigInt()}after</p>
|
|
15
|
+
<p>before{Date()}after</p>
|
|
16
|
+
</>
|
|
17
|
+
}
|
|
18
|
+
render(App);
|
|
19
|
+
expect([...container.querySelectorAll('b')].map((node) => node.textContent)).toEqual([
|
|
20
|
+
'string',
|
|
21
|
+
'number',
|
|
22
|
+
'boolean',
|
|
23
|
+
'bigint',
|
|
24
|
+
'date',
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps numeric addition, bigint conversion, and sequence effects during updates', () => {
|
|
29
|
+
let calls = 0;
|
|
30
|
+
function App() @{
|
|
31
|
+
const count = track(2);
|
|
32
|
+
<>
|
|
33
|
+
<p>sum: {Number(count.value) + Number(count.value)}</p>
|
|
34
|
+
<div>
|
|
35
|
+
{'big: '}
|
|
36
|
+
{(calls++, BigInt(count.value) satisfies bigint)}
|
|
37
|
+
</div>
|
|
38
|
+
<button onClick={() => count.value++}>update</button>
|
|
39
|
+
</>
|
|
40
|
+
}
|
|
41
|
+
render(App);
|
|
42
|
+
expect(container.querySelector('p').textContent).toBe('sum: 4');
|
|
43
|
+
expect(container.querySelector('div').textContent).toBe('big: 2');
|
|
44
|
+
expect(calls).toBe(1);
|
|
45
|
+
container.querySelector('button').click();
|
|
46
|
+
flushSync();
|
|
47
|
+
expect(container.querySelector('p').textContent).toBe('sum: 6');
|
|
48
|
+
expect(container.querySelector('div').textContent).toBe('big: 3');
|
|
49
|
+
expect(calls).toBe(2);
|
|
50
|
+
});
|