ripple 0.4.2 → 0.4.4
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 +164 -0
- package/package.json +10 -4
- package/src/constants.js +6 -0
- package/src/jsx-runtime.d.ts +2 -1
- package/src/runtime/array.js +15 -9
- package/src/runtime/index-client.js +25 -7
- package/src/runtime/index-server.js +1 -0
- package/src/runtime/internal/client/attributes.js +439 -0
- package/src/runtime/internal/client/blocks.js +20 -20
- package/src/runtime/internal/client/component.js +2 -5
- package/src/runtime/internal/client/composite.js +295 -95
- package/src/runtime/internal/client/constants.js +14 -6
- package/src/runtime/internal/client/context.js +3 -2
- package/src/runtime/internal/client/css.js +11 -26
- package/src/runtime/internal/client/errors.js +188 -0
- package/src/runtime/internal/client/events.js +79 -34
- package/src/runtime/internal/client/expression.js +25 -120
- package/src/runtime/internal/client/for.js +113 -262
- package/src/runtime/internal/client/head.js +4 -3
- package/src/runtime/internal/client/html.js +5 -3
- package/src/runtime/internal/client/hydrate.js +484 -0
- package/src/runtime/internal/client/hydration-enabled.js +7 -0
- package/src/runtime/internal/client/hydration.js +16 -81
- package/src/runtime/internal/client/if.js +69 -10
- package/src/runtime/internal/client/index.js +18 -10
- package/src/runtime/internal/client/operations.js +11 -7
- package/src/runtime/internal/client/portal.js +3 -2
- package/src/runtime/internal/client/render.js +42 -325
- package/src/runtime/internal/client/root-boundary-enabled.js +7 -0
- package/src/runtime/internal/client/runtime.js +157 -371
- package/src/runtime/internal/client/template-ns.js +88 -0
- package/src/runtime/internal/client/template.js +118 -139
- package/src/runtime/internal/client/track-async.js +356 -0
- package/src/runtime/internal/client/try.js +133 -319
- package/src/runtime/internal/client/types.d.ts +48 -24
- package/src/runtime/internal/server/index.js +31 -29
- package/src/runtime/internal/server/prerender.js +27 -0
- package/src/server/index.js +1 -0
- package/src/utils/class-name.js +28 -0
- package/tests/client/basic/basic.errors.test.tsrx +3 -3
- package/tests/client/compiler/__snapshots__/compiler.typescript.test.tsrx.snap +0 -2
- package/tests/client/compiler/compiler.basic.test.tsrx +2 -2
- package/tests/client/compiler/compiler.tracked-access.test.tsrx +3 -3
- package/tests/client/composite/composite.dynamic-components.test.tsrx +40 -0
- package/tests/client/composite/composite.props.test.tsrx +10 -10
- package/tests/client/css/scoped-styles.test.tsrx +3 -1
- package/tests/client/derived-release.test.tsrx +3 -3
- package/tests/client/for-item.test.tsrx +39 -0
- package/tests/client/for-selector.test.tsrx +95 -0
- package/tests/client/svg.test.tsrx +273 -1
- package/tests/hydration/compiled/client/basic.js +88 -87
- package/tests/hydration/compiled/client/composite.js +15 -6
- package/tests/hydration/compiled/client/events.js +40 -48
- package/tests/hydration/compiled/client/for.js +191 -199
- package/tests/hydration/compiled/client/fragment-cursor.js +633 -334
- package/tests/hydration/compiled/client/head.js +22 -22
- package/tests/hydration/compiled/client/hmr.js +14 -5
- package/tests/hydration/compiled/client/html-in-template.js +5 -5
- package/tests/hydration/compiled/client/html.js +195 -177
- package/tests/hydration/compiled/client/if-children.js +136 -63
- package/tests/hydration/compiled/client/if-fragment-controlflow.js +63 -43
- package/tests/hydration/compiled/client/if.js +78 -30
- package/tests/hydration/compiled/client/mixed-control-flow.js +90 -72
- package/tests/hydration/compiled/client/nested-control-flow.js +266 -307
- package/tests/hydration/compiled/client/portal.js +20 -11
- package/tests/hydration/compiled/client/reactivity.js +29 -29
- package/tests/hydration/compiled/client/return.js +2 -2
- package/tests/hydration/compiled/client/streaming.js +33 -35
- package/tests/hydration/compiled/client/switch.js +25 -25
- package/tests/hydration/compiled/client/track-async-serialization.js +41 -41
- package/tests/hydration/compiled/client/try.js +15 -15
- package/tests/hydration/compiled/client/typed-text.js +8 -7
- package/tests/hydration/compiled/server/basic.js +6 -6
- package/tests/hydration/compiled/server/events.js +8 -8
- package/tests/hydration/compiled/server/for.js +22 -22
- package/tests/hydration/compiled/server/fragment-cursor.js +188 -52
- package/tests/hydration/compiled/server/head.js +6 -6
- package/tests/hydration/compiled/server/hmr.js +1 -1
- package/tests/hydration/compiled/server/html.js +1 -1
- package/tests/hydration/compiled/server/if-children.js +9 -9
- package/tests/hydration/compiled/server/if.js +5 -5
- package/tests/hydration/compiled/server/mixed-control-flow.js +4 -4
- package/tests/hydration/compiled/server/portal.js +1 -1
- package/tests/hydration/compiled/server/reactivity.js +9 -9
- package/tests/hydration/compiled/server/streaming.js +9 -9
- package/tests/hydration/compiled/server/switch.js +4 -4
- package/tests/hydration/compiled/server/track-async-serialization.js +16 -16
- package/tests/hydration/compiled/server/try.js +4 -4
- package/tests/hydration/compiled/server/typed-text.js +1 -1
- package/tests/hydration/components/fragment-cursor.tsrx +60 -0
- package/tests/hydration/components/track-async-serialization.tsrx +2 -2
- package/tests/hydration/fragment-cursor.test.js +34 -1
- package/tests/hydration/track-async-serialization.test.js +1 -1
- package/tests/server/basic.components.test.tsrx +3 -3
- package/tests/server/track-async-serialization.test.tsrx +6 -6
- package/tests/utils/tracked-types.test.js +3 -3
- package/types/index.d.ts +8 -8
- package/types/server.d.ts +17 -0
|
@@ -48,6 +48,11 @@ export type AppendIntoAnchor = {
|
|
|
48
48
|
parent: Node;
|
|
49
49
|
/** Marks the sentinel; a missing-property read on a DOM node is cheaper than `in`. */
|
|
50
50
|
into: true;
|
|
51
|
+
/**
|
|
52
|
+
* The sentinel is the tail of `parent`: nothing the framework renders
|
|
53
|
+
* follows what it anchors, so an `@if` there never needs an anchor node.
|
|
54
|
+
*/
|
|
55
|
+
tail: boolean;
|
|
51
56
|
};
|
|
52
57
|
|
|
53
58
|
export type Block = {
|
|
@@ -77,30 +82,50 @@ export type TryPendingFunction = (anchor: Node) => void;
|
|
|
77
82
|
* helpers are module functions that compile only when a boundary uses them.
|
|
78
83
|
*/
|
|
79
84
|
export type TryState = {
|
|
80
|
-
anchor
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
/** anchor */
|
|
86
|
+
a: Node;
|
|
87
|
+
/** try function */
|
|
88
|
+
fn: (anchor: Node, block?: Block) => void;
|
|
89
|
+
/** catch function */
|
|
90
|
+
c: TryCatchFunction | null;
|
|
91
|
+
/** pending function */
|
|
92
|
+
p: TryPendingFunction | null;
|
|
84
93
|
/** the catch branch's `reset` callback, created on first use */
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
r: (() => void) | null;
|
|
95
|
+
/** pending request count */
|
|
96
|
+
n: number;
|
|
97
|
+
/** request version */
|
|
98
|
+
v: number;
|
|
99
|
+
/** active request ids */
|
|
100
|
+
q: Set<number>;
|
|
101
|
+
/** the try block */
|
|
102
|
+
b: Block | null;
|
|
103
|
+
/** resolved branch */
|
|
104
|
+
rb: Block | null;
|
|
105
|
+
/** pending branch */
|
|
106
|
+
pb: Block | null;
|
|
107
|
+
/** catch branch */
|
|
108
|
+
cb: Block | null;
|
|
109
|
+
/** fragment holding the resolved branch while it is offscreen */
|
|
110
|
+
o: DocumentFragment | null;
|
|
111
|
+
/** has resolved */
|
|
112
|
+
h: boolean;
|
|
113
|
+
/** mode: 0 resolved, 1 pending, 2 catch */
|
|
114
|
+
m: 0 | 1 | 2;
|
|
115
|
+
/** deferred rejections by request id */
|
|
116
|
+
d: Map<number, (reason: any) => void>;
|
|
117
|
+
/** blocks paused on a pending read */
|
|
118
|
+
z: Set<Block>;
|
|
98
119
|
/** a streamed slot this boundary hydrated, until its chunk activates it */
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
si: string | null;
|
|
121
|
+
/** the streamed slot errored */
|
|
122
|
+
se: boolean;
|
|
123
|
+
/** the streamed slot carries a hydratable fallback */
|
|
124
|
+
sf: boolean;
|
|
125
|
+
/** streamed slot open marker */
|
|
126
|
+
so: Comment | null;
|
|
127
|
+
/** streamed slot close marker */
|
|
128
|
+
sc: Comment | null;
|
|
104
129
|
};
|
|
105
130
|
|
|
106
131
|
export type BlockWithTryBoundary = Omit<Block, 's'> & {
|
|
@@ -108,7 +133,7 @@ export type BlockWithTryBoundary = Omit<Block, 's'> & {
|
|
|
108
133
|
};
|
|
109
134
|
|
|
110
135
|
export type BlockWithTryBoundaryAndCatch = Omit<BlockWithTryBoundary, 's'> & {
|
|
111
|
-
s: TryState & {
|
|
136
|
+
s: TryState & { c: TryCatchFunction };
|
|
112
137
|
};
|
|
113
138
|
|
|
114
139
|
export type RootBoundaryOptions = {
|
|
@@ -144,7 +169,6 @@ declare global {
|
|
|
144
169
|
|
|
145
170
|
interface Element {
|
|
146
171
|
__attributes?: {
|
|
147
|
-
checked?: boolean;
|
|
148
172
|
value?: string;
|
|
149
173
|
};
|
|
150
174
|
__click?: () => void;
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
is_boolean_attribute,
|
|
61
61
|
normalize_css_property_name,
|
|
62
62
|
} from '@tsrx/core/runtime/html';
|
|
63
|
-
import {
|
|
63
|
+
import { class_name } from '../../../utils/class-name.js';
|
|
64
64
|
import { create_ref_prop } from '@tsrx/core/runtime/ref';
|
|
65
65
|
import {
|
|
66
66
|
BLOCK_CLOSE,
|
|
@@ -1599,7 +1599,7 @@ export function attr(name, value, is_boolean = false) {
|
|
|
1599
1599
|
}
|
|
1600
1600
|
if (value == null || (!value && is_boolean)) return '';
|
|
1601
1601
|
const normalized = (name in replacements && replacements[name].get(value)) || value;
|
|
1602
|
-
let value_to_escape = name === 'class' ?
|
|
1602
|
+
let value_to_escape = name === 'class' ? class_name(normalized) : normalized;
|
|
1603
1603
|
value_to_escape =
|
|
1604
1604
|
name === 'style'
|
|
1605
1605
|
? typeof value !== 'string'
|
|
@@ -1719,15 +1719,6 @@ class TrackedValue {
|
|
|
1719
1719
|
set value(v) {
|
|
1720
1720
|
set(/** @type {Tracked} */ (this), v);
|
|
1721
1721
|
}
|
|
1722
|
-
/**
|
|
1723
|
-
* A read-only view: a derived over this value, for a receiver that should
|
|
1724
|
-
* read but not write it. Equivalent to `track(() => tracked.value)`. The
|
|
1725
|
-
* view has no hash of its own: it is not a serialized value.
|
|
1726
|
-
* @returns {Derived}
|
|
1727
|
-
*/
|
|
1728
|
-
readOnly() {
|
|
1729
|
-
return derived(() => get_tracked(/** @type {Tracked} */ (this)));
|
|
1730
|
-
}
|
|
1731
1722
|
}
|
|
1732
1723
|
|
|
1733
1724
|
class DerivedValue {
|
|
@@ -1765,17 +1756,6 @@ class DerivedValue {
|
|
|
1765
1756
|
set value(v) {
|
|
1766
1757
|
set(/** @type {Derived} */ (this), v);
|
|
1767
1758
|
}
|
|
1768
|
-
/**
|
|
1769
|
-
* A read-only view (see `TrackedValue#readOnly`). A derived without a
|
|
1770
|
-
* setter is already read-only and is returned as is; a writable one is
|
|
1771
|
-
* wrapped in a derived that follows it.
|
|
1772
|
-
* @returns {Derived}
|
|
1773
|
-
*/
|
|
1774
|
-
readOnly() {
|
|
1775
|
-
return this.a.set === undefined
|
|
1776
|
-
? /** @type {Derived} */ (this)
|
|
1777
|
-
: derived(() => get_derived(/** @type {Derived} */ (this)));
|
|
1778
|
-
}
|
|
1779
1759
|
}
|
|
1780
1760
|
|
|
1781
1761
|
/**
|
|
@@ -1804,6 +1784,25 @@ function derived(v, hash, get, set) {
|
|
|
1804
1784
|
);
|
|
1805
1785
|
}
|
|
1806
1786
|
|
|
1787
|
+
/**
|
|
1788
|
+
* A read-only view of a tracked or derived value (see the client runtime's
|
|
1789
|
+
* `track_read_only`). The view has no hash of its own: it is not a serialized
|
|
1790
|
+
* value.
|
|
1791
|
+
* @param {any} value
|
|
1792
|
+
* @returns {any}
|
|
1793
|
+
*/
|
|
1794
|
+
export function track_read_only(value) {
|
|
1795
|
+
if (!is_ripple_object(value)) {
|
|
1796
|
+
return value;
|
|
1797
|
+
}
|
|
1798
|
+
if ((value.f & DERIVED) !== 0) {
|
|
1799
|
+
var d = /** @type {Derived} */ (value);
|
|
1800
|
+
return d.a.set === undefined ? d : derived(() => get_derived(d));
|
|
1801
|
+
}
|
|
1802
|
+
var t = /** @type {Tracked} */ (value);
|
|
1803
|
+
return derived(() => get_tracked(t));
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1807
1806
|
/**
|
|
1808
1807
|
* @param {any} v
|
|
1809
1808
|
* @param {string} hash
|
|
@@ -2278,7 +2277,7 @@ function run_track_async(t, fn, block, dr, dj) {
|
|
|
2278
2277
|
/**
|
|
2279
2278
|
* The hashes of the compile-time tracked values a trackAsync read, recorded
|
|
2280
2279
|
* in its hydration envelope so the client re-subscribes to them. A dependency
|
|
2281
|
-
* without a hash that is itself a derived (a `
|
|
2280
|
+
* without a hash that is itself a derived (a `trackReadOnly()` view, a derived made
|
|
2282
2281
|
* outside a compiled call) stands for what it read: its own dependencies are
|
|
2283
2282
|
* recorded in its place.
|
|
2284
2283
|
* @param {Dependency | null} head
|
|
@@ -2530,35 +2529,38 @@ export function ripple_array(...elements) {
|
|
|
2530
2529
|
}
|
|
2531
2530
|
|
|
2532
2531
|
/**
|
|
2532
|
+
* `RippleArray.from(...)`
|
|
2533
2533
|
* @template T
|
|
2534
2534
|
* @param {ArrayLike<T> | Iterable<T>} arrayLike
|
|
2535
2535
|
* @param {(v: T, k: number) => any | undefined} [map_fn]
|
|
2536
2536
|
* @param {any} [thisArg]
|
|
2537
2537
|
* @returns {T[]}
|
|
2538
2538
|
*/
|
|
2539
|
-
|
|
2539
|
+
export function ripple_array_from(arrayLike, map_fn, thisArg) {
|
|
2540
2540
|
return map_fn ? Array.from(arrayLike, map_fn, thisArg) : Array.from(arrayLike);
|
|
2541
|
-
}
|
|
2541
|
+
}
|
|
2542
2542
|
|
|
2543
2543
|
/**
|
|
2544
|
+
* `RippleArray.of(...)`
|
|
2544
2545
|
* @template T
|
|
2545
2546
|
* @param {...T} items
|
|
2546
2547
|
* @returns {T[]}
|
|
2547
2548
|
*/
|
|
2548
|
-
|
|
2549
|
+
export function ripple_array_of(...items) {
|
|
2549
2550
|
return Array.of(...items);
|
|
2550
|
-
}
|
|
2551
|
+
}
|
|
2551
2552
|
|
|
2552
2553
|
/**
|
|
2554
|
+
* `RippleArray.fromAsync(...)`
|
|
2553
2555
|
* @template T
|
|
2554
2556
|
* @param {ArrayLike<T> | Iterable<T>} arrayLike
|
|
2555
2557
|
* @param {(v: T, k: number) => any | undefined} [map_fn]
|
|
2556
2558
|
* @param {any} [thisArg]
|
|
2557
2559
|
* @returns {Promise<T[]>}
|
|
2558
2560
|
*/
|
|
2559
|
-
|
|
2561
|
+
export async function ripple_array_from_async(arrayLike, map_fn, thisArg) {
|
|
2560
2562
|
return map_fn ? Array.fromAsync(arrayLike, map_fn, thisArg) : Array.fromAsync(arrayLike);
|
|
2561
|
-
}
|
|
2563
|
+
}
|
|
2562
2564
|
|
|
2563
2565
|
/**
|
|
2564
2566
|
* @param {object} obj
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** @import { RenderComponent } from './index.js' */
|
|
2
|
+
/** @import { RenderResult } from 'ripple/server' */
|
|
3
|
+
/** @import { RootBoundaryOptions } from '#public' */
|
|
4
|
+
|
|
5
|
+
import { render } from './index.js';
|
|
6
|
+
import { get_css_text } from './css-registry.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders a component to static HTML: buffered, every `@try` boundary and
|
|
10
|
+
* `trackAsync` settled before it resolves, with the scoped CSS as text. The
|
|
11
|
+
* static-generation counterpart of `render()`, for HTML written at build time
|
|
12
|
+
* that `hydrate()` picks up like any server-rendered page.
|
|
13
|
+
* @param {RenderComponent} component
|
|
14
|
+
* @param {{ rootBoundary?: RootBoundaryOptions }} [options]
|
|
15
|
+
* @returns {Promise<{ head: string, body: string, css: string, topLevelError: Error | null }>}
|
|
16
|
+
*/
|
|
17
|
+
export async function prerender(component, options = {}) {
|
|
18
|
+
const result = /** @type {RenderResult} */ (
|
|
19
|
+
await render(component, { rootBoundary: options.rootBoundary })
|
|
20
|
+
);
|
|
21
|
+
return {
|
|
22
|
+
head: result.head,
|
|
23
|
+
body: result.body,
|
|
24
|
+
css: get_css_text(result.css),
|
|
25
|
+
topLevelError: result.topLevelError ?? null,
|
|
26
|
+
};
|
|
27
|
+
}
|
package/src/server/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SSR helpers
|
|
2
2
|
export { create_ssr_stream as createStream, render } from '../runtime/internal/server/index.js';
|
|
3
3
|
export { get_css_text as getCss } from '../runtime/internal/server/css-registry.js';
|
|
4
|
+
export { prerender } from '../runtime/internal/server/prerender.js';
|
|
4
5
|
export { executeServerFunction } from '../runtime/internal/server/rpc.js';
|
|
5
6
|
|
|
6
7
|
// Re-export server runtime for components compiled for SSR
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clsx-style class composition, shared by the client and server renderers: a
|
|
3
|
+
* string as it is, a truthy number, the truthy items of an array, the keys of
|
|
4
|
+
* an object with truthy values, nested arrays flattened, everything else
|
|
5
|
+
* dropped.
|
|
6
|
+
* @param {any} value
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function class_name(value) {
|
|
10
|
+
if (typeof value === 'string') {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
if (typeof value !== 'object' || value === null) {
|
|
14
|
+
return typeof value === 'number' && value ? '' + value : '';
|
|
15
|
+
}
|
|
16
|
+
var str = '';
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
for (var i = 0; i < value.length; i++) {
|
|
19
|
+
var item = value[i] && class_name(value[i]);
|
|
20
|
+
if (item) str = str ? str + ' ' + item : item;
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
for (var key in value) {
|
|
24
|
+
if (value[key]) str = str ? str + ' ' + key : key;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return str;
|
|
28
|
+
}
|
|
@@ -126,7 +126,7 @@ describe('basic client > errors', () => {
|
|
|
126
126
|
|
|
127
127
|
render(Basic);
|
|
128
128
|
|
|
129
|
-
expect(error).
|
|
129
|
+
expect(error).toContain(
|
|
130
130
|
'Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation',
|
|
131
131
|
);
|
|
132
132
|
});
|
|
@@ -148,7 +148,7 @@ describe('basic client > errors', () => {
|
|
|
148
148
|
|
|
149
149
|
render(Basic);
|
|
150
150
|
|
|
151
|
-
expect(error).
|
|
151
|
+
expect(error).toContain(
|
|
152
152
|
'Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation',
|
|
153
153
|
);
|
|
154
154
|
});
|
|
@@ -169,7 +169,7 @@ describe('basic client > errors', () => {
|
|
|
169
169
|
|
|
170
170
|
render(Basic);
|
|
171
171
|
|
|
172
|
-
expect(error).
|
|
172
|
+
expect(error).toContain(
|
|
173
173
|
'Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation',
|
|
174
174
|
);
|
|
175
175
|
});
|
|
@@ -213,8 +213,8 @@ export function App() @{
|
|
|
213
213
|
|
|
214
214
|
// Already a string: no String(… ?? '') double wrap around it, and the
|
|
215
215
|
// scope wrapper sits on the user call, not on the coercion.
|
|
216
|
-
expect(code).
|
|
217
|
-
expect(code).not.
|
|
216
|
+
expect(code).toMatch(/String\(_\$_\.with_scope\(__prev\._[a-z]+, fetchLabel\)\)/);
|
|
217
|
+
expect(code).not.toMatch(/_\$_\.with_scope\(__prev\._[a-z]+, \(\) => String\(/);
|
|
218
218
|
expect(code).not.toContain('String(String(');
|
|
219
219
|
expect(code).not.toContain('?? \'\'');
|
|
220
220
|
});
|
|
@@ -5,7 +5,7 @@ const value_message = /Accessing a tracked object directly is not allowed, use `
|
|
|
5
5
|
describe('Compiler: Tracked Object Direct Access Checks', () => {
|
|
6
6
|
it('should error on direct access to __v of a tracked object', () => {
|
|
7
7
|
const code = `
|
|
8
|
-
import { track } from 'ripple';
|
|
8
|
+
import { track, trackReadOnly } from 'ripple';
|
|
9
9
|
export default function App() @{
|
|
10
10
|
let count = track(0);
|
|
11
11
|
console.log(count.__v);
|
|
@@ -200,12 +200,12 @@ import { track } from 'ripple';
|
|
|
200
200
|
expect(() => compile(code, 'test.tsrx')).not.toThrow();
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
it('should allow .
|
|
203
|
+
it('should allow .trackReadOnly() on a tracked object', () => {
|
|
204
204
|
const code = `
|
|
205
205
|
import { track } from 'ripple';
|
|
206
206
|
export default function App() @{
|
|
207
207
|
let count = track(0);
|
|
208
|
-
const view = count
|
|
208
|
+
const view = trackReadOnly(count);
|
|
209
209
|
<>{view.value}</>
|
|
210
210
|
}
|
|
211
211
|
`;
|
|
@@ -76,6 +76,46 @@ describe('composite > dynamic components', () => {
|
|
|
76
76
|
expect(container.textContent).toBe('hidden');
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
it(
|
|
80
|
+
'remounts a childless dynamic component with fresh props when its tag expression re-evaluates',
|
|
81
|
+
() => {
|
|
82
|
+
let bump: () => void = () => {};
|
|
83
|
+
let poke: () => void = () => {};
|
|
84
|
+
|
|
85
|
+
function Child(props: { n: number }) @{
|
|
86
|
+
<span>{props.n}</span>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function App() @{
|
|
90
|
+
const n = track(0);
|
|
91
|
+
const flag = track(true);
|
|
92
|
+
bump = () => {
|
|
93
|
+
n.value++;
|
|
94
|
+
};
|
|
95
|
+
poke = () => {
|
|
96
|
+
flag.value = !flag.value;
|
|
97
|
+
};
|
|
98
|
+
// The tag expression reads `flag`, but always resolves to `Child`.
|
|
99
|
+
<{flag.value || !flag.value ? Child : null} n={n.value} />
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
render(App);
|
|
103
|
+
flushSync();
|
|
104
|
+
const span = () => container.querySelector('span');
|
|
105
|
+
expect(span()?.textContent).toBe('0');
|
|
106
|
+
|
|
107
|
+
// A prop is a value: the instance keeps the number it got.
|
|
108
|
+
bump();
|
|
109
|
+
flushSync();
|
|
110
|
+
expect(span()?.textContent).toBe('0');
|
|
111
|
+
|
|
112
|
+
// The tag expression re-evaluated: a new instance reads the literal again.
|
|
113
|
+
poke();
|
|
114
|
+
flushSync();
|
|
115
|
+
expect(span()?.textContent).toBe('1');
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
79
119
|
it('handles dynamic component switching', () => {
|
|
80
120
|
function Child1() @{
|
|
81
121
|
<div>{'I am child 1'}</div>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Derived, Tracked, Props } from 'ripple';
|
|
2
|
-
import { effect, flushSync, track } from 'ripple';
|
|
2
|
+
import { effect, flushSync, track, trackReadOnly } from 'ripple';
|
|
3
3
|
|
|
4
4
|
describe('composite > props', () => {
|
|
5
|
-
it('hands a child a read-only view through
|
|
5
|
+
it('hands a child a read-only view through trackReadOnly()', () => {
|
|
6
6
|
function Child({ count }: { count: Derived<number> }) @{
|
|
7
7
|
<span class="child">{count.value}</span>
|
|
8
8
|
}
|
|
@@ -10,7 +10,7 @@ describe('composite > props', () => {
|
|
|
10
10
|
function App() @{
|
|
11
11
|
const count = track(0);
|
|
12
12
|
<>
|
|
13
|
-
<Child count={count
|
|
13
|
+
<Child count={trackReadOnly(count)} />
|
|
14
14
|
<button onClick={() => count.value++}>{'inc'}</button>
|
|
15
15
|
</>
|
|
16
16
|
}
|
|
@@ -24,13 +24,13 @@ describe('composite > props', () => {
|
|
|
24
24
|
expect(container.querySelector('.child').textContent).toBe('1');
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it('returns a read-only derived from
|
|
27
|
+
it('returns a read-only derived from trackReadOnly() as is and wraps a writable one', () => {
|
|
28
28
|
function App() @{
|
|
29
29
|
const count = track(1);
|
|
30
30
|
const double = track(() => count.value * 2);
|
|
31
31
|
const draft = track(() => count.value, undefined, true);
|
|
32
|
-
const double_view = double
|
|
33
|
-
const draft_view = draft
|
|
32
|
+
const double_view = trackReadOnly(double);
|
|
33
|
+
const draft_view = trackReadOnly(draft);
|
|
34
34
|
expect(double_view).toBe(double);
|
|
35
35
|
expect(draft_view).not.toBe(draft);
|
|
36
36
|
<>
|
|
@@ -51,7 +51,7 @@ describe('composite > props', () => {
|
|
|
51
51
|
expect(container.querySelector('.view').textContent).toBe('42');
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it('releases a
|
|
54
|
+
it('releases a trackReadOnly() view with the block that created it', () => {
|
|
55
55
|
let set_rows: (rows: number[]) => void = () => {};
|
|
56
56
|
let bump: () => void = () => {};
|
|
57
57
|
let source: Tracked<number> | undefined;
|
|
@@ -73,7 +73,7 @@ describe('composite > props', () => {
|
|
|
73
73
|
<ul>
|
|
74
74
|
@for (const row of rows.value; key row) {
|
|
75
75
|
<li>
|
|
76
|
-
<Child count={count
|
|
76
|
+
<Child count={trackReadOnly(count)} />
|
|
77
77
|
</li>
|
|
78
78
|
}
|
|
79
79
|
</ul>
|
|
@@ -106,12 +106,12 @@ describe('composite > props', () => {
|
|
|
106
106
|
expect(subscribers()).toBe(0);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
it('keeps a
|
|
109
|
+
it('keeps a trackReadOnly() view from writing through to its source', () => {
|
|
110
110
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
111
111
|
try {
|
|
112
112
|
function App() @{
|
|
113
113
|
const count = track(1);
|
|
114
|
-
const view = count
|
|
114
|
+
const view = trackReadOnly(count);
|
|
115
115
|
const write = () => {
|
|
116
116
|
view.value = 99;
|
|
117
117
|
};
|
|
@@ -645,8 +645,10 @@ export function Precedence() @{
|
|
|
645
645
|
// block is written after the nested scope.
|
|
646
646
|
expect(black).toBeLessThan(italic);
|
|
647
647
|
expect(italic).toBeLessThan(blue);
|
|
648
|
+
// The outer div shares a fragment template with the placeholder that
|
|
649
|
+
// follows it; the inner div is a flat element template of its own.
|
|
648
650
|
expect(code).toContain(`class="outer ${outer} ${theme_hash}"`);
|
|
649
|
-
expect(code).toContain(`class
|
|
651
|
+
expect(code).toContain(`'class', 'inner ${outer} ${inner} ${theme_hash}'`);
|
|
650
652
|
},
|
|
651
653
|
);
|
|
652
654
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Derived, Tracked } from 'ripple';
|
|
2
|
-
import { effect, flushSync, track } from 'ripple';
|
|
2
|
+
import { effect, flushSync, track, trackReadOnly } from 'ripple';
|
|
3
3
|
|
|
4
4
|
/** The runtime's own subscriber list of a tracked value (`sb` / `sn`). */
|
|
5
5
|
function subscribers(source: Tracked<number>) {
|
|
@@ -34,7 +34,7 @@ describe('derived release', () => {
|
|
|
34
34
|
const label = track(() => 'n=' + count.value);
|
|
35
35
|
<>
|
|
36
36
|
<span class="label">{label.value}</span>
|
|
37
|
-
<Child count={count
|
|
37
|
+
<Child count={trackReadOnly(count)} />
|
|
38
38
|
</>
|
|
39
39
|
}
|
|
40
40
|
</>
|
|
@@ -73,7 +73,7 @@ describe('derived release', () => {
|
|
|
73
73
|
<ul>
|
|
74
74
|
@for (const row of rows.value; key row) {
|
|
75
75
|
<li>
|
|
76
|
-
<Child count={count
|
|
76
|
+
<Child count={trackReadOnly(count)} />
|
|
77
77
|
</li>
|
|
78
78
|
}
|
|
79
79
|
</ul>
|
|
@@ -270,6 +270,45 @@ describe('trailing control flow appends into the parent', () => {
|
|
|
270
270
|
expect(div.lastElementChild?.localName).toBe('span');
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
+
it('leaves no anchor node behind a trailing @if', () => {
|
|
274
|
+
let set: (value: boolean) => void = () => {};
|
|
275
|
+
|
|
276
|
+
function App() @{
|
|
277
|
+
const open = track(false);
|
|
278
|
+
set = (value) => {
|
|
279
|
+
open.value = value;
|
|
280
|
+
};
|
|
281
|
+
<div>
|
|
282
|
+
<b>{'head'}</b>
|
|
283
|
+
@if (open.value) {
|
|
284
|
+
<span>{'open'}</span>
|
|
285
|
+
}
|
|
286
|
+
</div>
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
render(App);
|
|
290
|
+
flushSync();
|
|
291
|
+
const div = container.querySelector('div')!;
|
|
292
|
+
// The if is the div's tail: nothing follows it, so it needs no anchor,
|
|
293
|
+
// off or on.
|
|
294
|
+
expect(div.childNodes.length).toBe(1);
|
|
295
|
+
|
|
296
|
+
set(true);
|
|
297
|
+
flushSync();
|
|
298
|
+
expect(div.innerHTML).toBe('<b>head</b><span>open</span>');
|
|
299
|
+
expect(div.childNodes.length).toBe(2);
|
|
300
|
+
|
|
301
|
+
set(false);
|
|
302
|
+
flushSync();
|
|
303
|
+
expect(div.innerHTML).toBe('<b>head</b>');
|
|
304
|
+
expect(div.childNodes.length).toBe(1);
|
|
305
|
+
|
|
306
|
+
set(true);
|
|
307
|
+
flushSync();
|
|
308
|
+
expect(div.innerHTML).toBe('<b>head</b><span>open</span>');
|
|
309
|
+
expect(div.childNodes.length).toBe(2);
|
|
310
|
+
});
|
|
311
|
+
|
|
273
312
|
it('keeps a trailing @for with @empty in place', () => {
|
|
274
313
|
let set: (items: string[]) => void = () => {};
|
|
275
314
|
|
|
@@ -219,3 +219,98 @@ describe('keyed reconciliation with most items moving', () => {
|
|
|
219
219
|
expect(after.map((el) => el.textContent)).toEqual(order.map(String));
|
|
220
220
|
});
|
|
221
221
|
});
|
|
222
|
+
|
|
223
|
+
// An `@if (outer === item.key)` in a keyed loop body lowers to the same
|
|
224
|
+
// selector as an attribute comparison would, reading the block's key: a
|
|
225
|
+
// change of the outer value re-runs the ifs of its previous and next key
|
|
226
|
+
// alone, and a replaced item never re-runs its if.
|
|
227
|
+
describe('@if selector comparisons', () => {
|
|
228
|
+
interface Item {
|
|
229
|
+
id: number;
|
|
230
|
+
label: string;
|
|
231
|
+
}
|
|
232
|
+
let select: (id: number | undefined) => void = () => {};
|
|
233
|
+
let setItems: (items: Item[]) => void = () => {};
|
|
234
|
+
|
|
235
|
+
function App() @{
|
|
236
|
+
const items = track<Item[]>([
|
|
237
|
+
{ id: 1, label: 'one' },
|
|
238
|
+
{ id: 2, label: 'two' },
|
|
239
|
+
{ id: 3, label: 'three' },
|
|
240
|
+
]);
|
|
241
|
+
const selected = track<number | undefined>(undefined);
|
|
242
|
+
select = (id) => {
|
|
243
|
+
selected.value = id;
|
|
244
|
+
};
|
|
245
|
+
setItems = (next) => {
|
|
246
|
+
items.value = next;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
<ul>
|
|
250
|
+
@for (const item of items.value; key item.id) {
|
|
251
|
+
<li data-id={item.id}>
|
|
252
|
+
<span>{item.label}</span>
|
|
253
|
+
@if (selected.value === item.id) {
|
|
254
|
+
<b>{'current'}</b>
|
|
255
|
+
}
|
|
256
|
+
</li>
|
|
257
|
+
}
|
|
258
|
+
</ul>
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function marked(): string[] {
|
|
262
|
+
return Array.from(container.querySelectorAll('li')).map(
|
|
263
|
+
(li) => `${li.getAttribute('data-id')}:${li.textContent}`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
it('moves the marker between rows and keeps it on a replaced row', () => {
|
|
268
|
+
render(App);
|
|
269
|
+
flushSync();
|
|
270
|
+
expect(marked()).toEqual(['1:one', '2:two', '3:three']);
|
|
271
|
+
|
|
272
|
+
select(2);
|
|
273
|
+
flushSync();
|
|
274
|
+
expect(marked()).toEqual(['1:one', '2:twocurrent', '3:three']);
|
|
275
|
+
const b = container.querySelector('b')!;
|
|
276
|
+
|
|
277
|
+
select(3);
|
|
278
|
+
flushSync();
|
|
279
|
+
expect(marked()).toEqual(['1:one', '2:two', '3:threecurrent']);
|
|
280
|
+
|
|
281
|
+
// The selected row's object is replaced under its key: the label
|
|
282
|
+
// updates and the marker stays, in place behind the label.
|
|
283
|
+
setItems([
|
|
284
|
+
{ id: 1, label: 'uno' },
|
|
285
|
+
{ id: 2, label: 'dos' },
|
|
286
|
+
{ id: 3, label: 'tres' },
|
|
287
|
+
]);
|
|
288
|
+
flushSync();
|
|
289
|
+
expect(marked()).toEqual(['1:uno', '2:dos', '3:trescurrent']);
|
|
290
|
+
expect(container.querySelector('li:last-child')!.childNodes.length).toBe(2);
|
|
291
|
+
expect(container.querySelector('b')).not.toBe(b);
|
|
292
|
+
|
|
293
|
+
select(undefined);
|
|
294
|
+
flushSync();
|
|
295
|
+
expect(marked()).toEqual(['1:uno', '2:dos', '3:tres']);
|
|
296
|
+
// Off again, and no anchor node is left behind the label.
|
|
297
|
+
for (const li of container.querySelectorAll('li')) {
|
|
298
|
+
expect(li.childNodes.length).toBe(1);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('marks a row created after the selection was set', () => {
|
|
303
|
+
render(App);
|
|
304
|
+
flushSync();
|
|
305
|
+
select(4);
|
|
306
|
+
flushSync();
|
|
307
|
+
expect(marked()).toEqual(['1:one', '2:two', '3:three']);
|
|
308
|
+
|
|
309
|
+
setItems([
|
|
310
|
+
{ id: 4, label: 'four' },
|
|
311
|
+
{ id: 1, label: 'one' },
|
|
312
|
+
]);
|
|
313
|
+
flushSync();
|
|
314
|
+
expect(marked()).toEqual(['4:fourcurrent', '1:one']);
|
|
315
|
+
});
|
|
316
|
+
});
|