ripple 0.3.47 → 0.3.49
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 +24 -0
- package/package.json +5 -5
- package/src/compiler/types/import.d.ts +2 -0
- package/src/runtime/index-client.js +3 -1
- package/src/runtime/index-server.js +2 -1
- package/src/runtime/internal/client/bindings.js +2 -1
- package/src/runtime/internal/client/blocks.js +9 -2
- package/src/runtime/internal/client/constants.js +1 -1
- package/src/runtime/internal/client/events.js +1 -1
- package/src/runtime/internal/client/for.js +1 -1
- package/src/runtime/internal/client/html.js +1 -1
- package/src/runtime/internal/client/index.js +3 -2
- package/src/runtime/internal/client/operations.js +1 -1
- package/src/runtime/internal/client/portal.js +1 -7
- package/src/runtime/internal/client/render.js +80 -24
- package/src/runtime/internal/client/runtime.js +17 -6
- package/src/runtime/internal/client/try.js +1 -9
- package/src/runtime/internal/client/utils.js +0 -39
- package/src/runtime/internal/server/index.js +6 -3
- package/src/runtime/proxy.js +1 -1
- package/tests/client/async-suspend.test.tsrx +3 -3
- package/tests/client/basic/basic.events.test.tsrx +30 -0
- package/tests/client/dynamic-elements.test.tsrx +79 -1
- package/tests/client/ref.test.tsrx +253 -13
- package/types/index.d.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# ripple
|
|
2
2
|
|
|
3
|
+
## 0.3.49
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1071](https://github.com/Ripple-TS/ripple/pull/1071)
|
|
8
|
+
[`b54a72f`](https://github.com/Ripple-TS/ripple/commit/b54a72f721adb5f08a5bf3e3d006780b7e1eb471)
|
|
9
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Add named ref props with
|
|
10
|
+
`prop_name={ref expr}` syntax and expose `isRefProp()` for runtime detection of
|
|
11
|
+
named ref prop values.
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
[[`b54a72f`](https://github.com/Ripple-TS/ripple/commit/b54a72f721adb5f08a5bf3e3d006780b7e1eb471),
|
|
14
|
+
[`b54a72f`](https://github.com/Ripple-TS/ripple/commit/b54a72f721adb5f08a5bf3e3d006780b7e1eb471),
|
|
15
|
+
[`b54a72f`](https://github.com/Ripple-TS/ripple/commit/b54a72f721adb5f08a5bf3e3d006780b7e1eb471)]:
|
|
16
|
+
- ripple@0.3.49
|
|
17
|
+
- @tsrx/core@0.0.28
|
|
18
|
+
- @tsrx/ripple@0.0.30
|
|
19
|
+
|
|
20
|
+
## 0.3.48
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies []:
|
|
25
|
+
- ripple@0.3.48
|
|
26
|
+
|
|
3
27
|
## 0.3.47
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.49",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -76,17 +76,17 @@
|
|
|
76
76
|
"esm-env": "^1.2.2",
|
|
77
77
|
"@types/estree": "^1.0.8",
|
|
78
78
|
"@types/estree-jsx": "^1.0.5",
|
|
79
|
-
"@tsrx/
|
|
79
|
+
"@tsrx/core": "0.0.28",
|
|
80
|
+
"@tsrx/ripple": "0.0.30"
|
|
80
81
|
},
|
|
81
82
|
"devDependencies": {
|
|
82
83
|
"@types/node": "^24.3.0",
|
|
83
84
|
"@typescript-eslint/types": "^8.40.0",
|
|
84
85
|
"typescript": "^5.9.3",
|
|
85
86
|
"@volar/language-core": "~2.4.28",
|
|
86
|
-
"vscode-languageserver-types": "^3.17.5"
|
|
87
|
-
"@tsrx/core": "0.0.27"
|
|
87
|
+
"vscode-languageserver-types": "^3.17.5"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
|
-
"ripple": "0.3.
|
|
90
|
+
"ripple": "0.3.49"
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
RippleDate as _$_Date__Ripple,
|
|
40
40
|
createRefKey as _$_RefKey__create,
|
|
41
41
|
} from 'ripple';
|
|
42
|
+
import { create_ref_prop as _$_RefProp__create } from '@tsrx/core/runtime/ref';
|
|
42
43
|
|
|
43
44
|
export {
|
|
44
45
|
_$_Map__Ripple,
|
|
@@ -49,4 +50,5 @@ export {
|
|
|
49
50
|
_$_URLSearchParams__Ripple,
|
|
50
51
|
_$_Date__Ripple,
|
|
51
52
|
_$_RefKey__create,
|
|
53
|
+
_$_RefProp__create,
|
|
52
54
|
};
|
|
@@ -24,7 +24,7 @@ import { COMMENT_NODE, HYDRATION_START } from '../constants.js';
|
|
|
24
24
|
export { jsx, jsxs, Fragment } from '../jsx-runtime.js';
|
|
25
25
|
export {
|
|
26
26
|
UNINITIALIZED,
|
|
27
|
-
|
|
27
|
+
TRACKED_UPDATED,
|
|
28
28
|
SUSPENSE_PENDING,
|
|
29
29
|
SUSPENSE_REJECTED,
|
|
30
30
|
} from './internal/client/constants.js';
|
|
@@ -156,6 +156,8 @@ export { Portal } from './internal/client/portal.js';
|
|
|
156
156
|
|
|
157
157
|
export { ref_prop as createRefKey, get, public_set as set } from './internal/client/runtime.js';
|
|
158
158
|
|
|
159
|
+
export { isRefProp } from '@tsrx/core/runtime/ref';
|
|
160
|
+
|
|
159
161
|
export { on } from './internal/client/events.js';
|
|
160
162
|
|
|
161
163
|
export {
|
|
@@ -12,10 +12,11 @@ export {
|
|
|
12
12
|
} from './internal/server/index.js';
|
|
13
13
|
export {
|
|
14
14
|
UNINITIALIZED,
|
|
15
|
-
|
|
15
|
+
TRACKED_UPDATED,
|
|
16
16
|
SUSPENSE_PENDING,
|
|
17
17
|
SUSPENSE_REJECTED,
|
|
18
18
|
} from './internal/client/constants.js';
|
|
19
|
+
export { isRefProp } from '@tsrx/core/runtime/ref';
|
|
19
20
|
|
|
20
21
|
export const effect = noop;
|
|
21
22
|
export const createRefKey = noop;
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
import { effect, render } from './blocks.js';
|
|
9
9
|
import { on } from './events.js';
|
|
10
10
|
import { get, set } from './runtime.js';
|
|
11
|
-
import {
|
|
11
|
+
import { is_ripple_object } from './utils.js';
|
|
12
|
+
import { is_array } from '@tsrx/core/runtime/language-helpers';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @param {string} name
|
|
@@ -105,7 +105,7 @@ export function branch(fn, flags = 0, state = null) {
|
|
|
105
105
|
* - a `Tracked` (e.g. from `track()`) — `tracked.value` is set to the
|
|
106
106
|
* element on mount and reset to `null` on unmount.
|
|
107
107
|
* - a plain mutable var (`let foo;`) — the element is assigned to the
|
|
108
|
-
* variable
|
|
108
|
+
* variable on mount and reset to `null` on unmount.
|
|
109
109
|
*
|
|
110
110
|
* `get_fn` is invoked through `untrack` so the surrounding render block
|
|
111
111
|
* doesn't subscribe to whatever the thunk happens to read. The supported
|
|
@@ -148,7 +148,14 @@ export function ref(element, get_fn, set_fn) {
|
|
|
148
148
|
});
|
|
149
149
|
});
|
|
150
150
|
} else if (set_fn !== undefined) {
|
|
151
|
-
|
|
151
|
+
e = branch(() => {
|
|
152
|
+
effect(() => {
|
|
153
|
+
set_fn(element);
|
|
154
|
+
return () => {
|
|
155
|
+
set_fn(null);
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
});
|
|
152
159
|
}
|
|
153
160
|
}
|
|
154
161
|
});
|
|
@@ -42,7 +42,7 @@ export const NAMESPACE_URI = {
|
|
|
42
42
|
mathml: 'http://www.w3.org/1998/Math/MathML',
|
|
43
43
|
};
|
|
44
44
|
/** @type {unique symbol} */
|
|
45
|
-
export const
|
|
45
|
+
export const TRACKED_UPDATED = Symbol('TRACKED_UPDATED');
|
|
46
46
|
/** @type {unique symbol} */
|
|
47
47
|
export const SUSPENSE_PENDING = Symbol('suspense_pending');
|
|
48
48
|
/** @type {unique symbol} */
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
set_tracking,
|
|
17
17
|
tracking,
|
|
18
18
|
} from './runtime.js';
|
|
19
|
-
import { array_from, define_property, is_array } from '
|
|
19
|
+
import { array_from, define_property, is_array } from '@tsrx/core/runtime/language-helpers';
|
|
20
20
|
import { render } from './blocks.js';
|
|
21
21
|
|
|
22
22
|
/** @type {Set<string>} */
|
|
@@ -6,7 +6,7 @@ import { FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
|
|
|
6
6
|
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
|
|
7
7
|
import { create_text, get_first_child, get_last_child, next_sibling } from './operations.js';
|
|
8
8
|
import { active_block, set, tracked, untrack } from './runtime.js';
|
|
9
|
-
import { array_from, is_array } from '
|
|
9
|
+
import { array_from, is_array } from '@tsrx/core/runtime/language-helpers';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @template V
|
|
@@ -4,7 +4,7 @@ import { remove_block_dom, render } from './blocks.js';
|
|
|
4
4
|
import { get_first_child, get_next_sibling } from './operations.js';
|
|
5
5
|
import { active_block } from './runtime.js';
|
|
6
6
|
import { assign_nodes, create_fragment_from_html } from './template.js';
|
|
7
|
-
import { hydrate_next,
|
|
7
|
+
import { hydrate_next, hydrating, set_hydrate_node } from './hydration.js';
|
|
8
8
|
import { COMMENT_NODE } from '../../../constants.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -32,7 +32,7 @@ export {
|
|
|
32
32
|
|
|
33
33
|
export {
|
|
34
34
|
UNINITIALIZED,
|
|
35
|
-
|
|
35
|
+
TRACKED_UPDATED,
|
|
36
36
|
SUSPENSE_PENDING,
|
|
37
37
|
SUSPENSE_REJECTED,
|
|
38
38
|
} from './constants.js';
|
|
@@ -65,6 +65,7 @@ export {
|
|
|
65
65
|
pop_component,
|
|
66
66
|
untrack,
|
|
67
67
|
ref_prop,
|
|
68
|
+
create_ref_prop,
|
|
68
69
|
fallback,
|
|
69
70
|
exclude_from_object,
|
|
70
71
|
derived,
|
|
@@ -89,7 +90,7 @@ export { switch_block as switch } from './switch.js';
|
|
|
89
90
|
|
|
90
91
|
export { template, append, text } from './template.js';
|
|
91
92
|
|
|
92
|
-
export { array_slice } from '
|
|
93
|
+
export { array_slice } from '@tsrx/core/runtime/language-helpers';
|
|
93
94
|
|
|
94
95
|
export { ripple_array } from '../../array.js';
|
|
95
96
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TEXT_NODE } from '../../../constants.js';
|
|
2
2
|
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
|
|
3
|
-
import { get_descriptor } from '
|
|
3
|
+
import { get_descriptor } from '@tsrx/core/runtime/language-helpers';
|
|
4
4
|
|
|
5
5
|
/** @type {(() => Node | null)} */
|
|
6
6
|
var first_child_getter;
|
|
@@ -5,13 +5,7 @@ import { UNINITIALIZED } from './constants.js';
|
|
|
5
5
|
import { handle_root_events } from './events.js';
|
|
6
6
|
import { create_text } from './operations.js';
|
|
7
7
|
import { active_block } from './runtime.js';
|
|
8
|
-
import {
|
|
9
|
-
hydrating,
|
|
10
|
-
hydrate_next,
|
|
11
|
-
hydrate_node,
|
|
12
|
-
set_hydrating,
|
|
13
|
-
set_hydrate_node,
|
|
14
|
-
} from './hydration.js';
|
|
8
|
+
import { hydrating, hydrate_node, set_hydrating, set_hydrate_node } from './hydration.js';
|
|
15
9
|
import { is_tsrx_element } from '../../element.js';
|
|
16
10
|
|
|
17
11
|
/**
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/** @import { Block } from '#client' */
|
|
2
2
|
|
|
3
|
-
import { destroy_block, ref } from './blocks.js';
|
|
3
|
+
import { branch, destroy_block, ref } from './blocks.js';
|
|
4
4
|
import { DESTROYED, REF_PROP } from './constants.js';
|
|
5
|
+
import { isRefProp as is_ref_prop } from '@tsrx/core/runtime/ref';
|
|
6
|
+
import { is_ripple_object } from './utils.js';
|
|
5
7
|
import {
|
|
6
8
|
get_descriptors,
|
|
7
9
|
get_own_property_symbols,
|
|
8
10
|
get_prototype_of,
|
|
9
|
-
|
|
10
|
-
} from './utils.js';
|
|
11
|
+
} from '@tsrx/core/runtime/language-helpers';
|
|
11
12
|
import { event } from './events.js';
|
|
12
13
|
import {
|
|
13
14
|
getAttributeEventName as get_attribute_event_name,
|
|
@@ -143,11 +144,14 @@ function set_attribute_helper(element, key, value, remove_listeners, prev) {
|
|
|
143
144
|
element.classList.add(value);
|
|
144
145
|
} else if (typeof key === 'string' && is_event_attribute(key)) {
|
|
145
146
|
// Handle event handlers in spread props
|
|
146
|
-
const event_name = get_attribute_event_name(key, value);
|
|
147
147
|
if (remove_listeners[key]) {
|
|
148
148
|
remove_listeners[key]();
|
|
149
|
+
remove_listeners[key] = undefined;
|
|
150
|
+
}
|
|
151
|
+
if (value != null) {
|
|
152
|
+
const event_name = get_attribute_event_name(key, value);
|
|
153
|
+
remove_listeners[key] = event(event_name, element, value);
|
|
149
154
|
}
|
|
150
|
-
remove_listeners[key] = event(event_name, element, value);
|
|
151
155
|
} else {
|
|
152
156
|
set_attribute(element, key, value);
|
|
153
157
|
}
|
|
@@ -254,56 +258,90 @@ export function set_selected(element, selected) {
|
|
|
254
258
|
export function apply_element_spread(element, fn) {
|
|
255
259
|
/** @type {Record<string | symbol, any>} */
|
|
256
260
|
var prev = {};
|
|
257
|
-
/** @type {Record<symbol, Block | undefined>} */
|
|
261
|
+
/** @type {Record<string | symbol, Block | undefined>} */
|
|
258
262
|
var effects = {};
|
|
259
263
|
/** @type {Record<string | symbol, (() => void) | undefined>} */
|
|
260
264
|
var remove_listeners = {};
|
|
261
265
|
|
|
262
266
|
/** @type {Record<symbol, any>} */
|
|
263
267
|
var prev_symbols = {};
|
|
268
|
+
/** @type {Record<string, any>} */
|
|
269
|
+
var prev_ref_props = {};
|
|
264
270
|
|
|
265
271
|
return () => {
|
|
266
272
|
var next = fn();
|
|
267
|
-
|
|
268
|
-
for (const symbol of get_own_property_symbols(effects)) {
|
|
269
|
-
if (!next[symbol] && effects[symbol]) {
|
|
270
|
-
destroy_block(effects[symbol]);
|
|
271
|
-
effects[symbol] = undefined;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** @type {Record<symbol, any>} */
|
|
276
|
-
var current_symbols = {};
|
|
273
|
+
var current_symbols = /** @type {Record<symbol, any>} */ ({});
|
|
277
274
|
|
|
278
275
|
for (const symbol of get_own_property_symbols(next)) {
|
|
279
|
-
|
|
276
|
+
if (symbol.description !== REF_PROP) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const ref_fn = next[symbol];
|
|
280
280
|
current_symbols[symbol] = ref_fn;
|
|
281
281
|
|
|
282
282
|
if (
|
|
283
|
-
symbol
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
(effects[symbol] && (effects[symbol].f & DESTROYED) !== 0))
|
|
283
|
+
!(symbol in prev_symbols) ||
|
|
284
|
+
ref_fn !== prev_symbols[symbol] ||
|
|
285
|
+
(effects[symbol] && (effects[symbol].f & DESTROYED) !== 0)
|
|
287
286
|
) {
|
|
288
287
|
if (effects[symbol] && (effects[symbol].f & DESTROYED) === 0) {
|
|
289
288
|
destroy_block(effects[symbol]);
|
|
290
289
|
}
|
|
291
|
-
effects[symbol] =
|
|
290
|
+
effects[symbol] = create_spread_ref_effect(element, ref_fn);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const symbol of get_own_property_symbols(prev_symbols)) {
|
|
295
|
+
if (!(symbol in current_symbols) && effects[symbol]) {
|
|
296
|
+
destroy_block(/** @type {Block} */ (effects[symbol]));
|
|
297
|
+
effects[symbol] = undefined;
|
|
292
298
|
}
|
|
293
299
|
}
|
|
294
300
|
|
|
295
301
|
prev_symbols = current_symbols;
|
|
296
302
|
|
|
303
|
+
/** @type {Record<string, any>} */
|
|
304
|
+
var current_ref_props = {};
|
|
305
|
+
|
|
306
|
+
for (const key in next) {
|
|
307
|
+
const ref_fn = next[key];
|
|
308
|
+
if (!is_ref_prop(ref_fn)) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
current_ref_props[key] = ref_fn;
|
|
313
|
+
|
|
314
|
+
if (
|
|
315
|
+
!(key in prev_ref_props) ||
|
|
316
|
+
ref_fn !== prev_ref_props[key] ||
|
|
317
|
+
(effects[key] && (effects[key].f & DESTROYED) !== 0)
|
|
318
|
+
) {
|
|
319
|
+
if (effects[key] && (effects[key].f & DESTROYED) === 0) {
|
|
320
|
+
destroy_block(effects[key]);
|
|
321
|
+
}
|
|
322
|
+
effects[key] = create_spread_ref_effect(element, ref_fn);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const key in prev_ref_props) {
|
|
327
|
+
if (!(key in current_ref_props) && effects[key]) {
|
|
328
|
+
destroy_block(/** @type {Block} */ (effects[key]));
|
|
329
|
+
effects[key] = undefined;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
prev_ref_props = current_ref_props;
|
|
334
|
+
|
|
297
335
|
for (let key in remove_listeners) {
|
|
298
336
|
// Remove event listeners that are no longer present
|
|
299
|
-
if (!(key in next) && remove_listeners[key]) {
|
|
337
|
+
if ((!(key in next) || is_ref_prop(next[key])) && remove_listeners[key]) {
|
|
300
338
|
remove_listeners[key]();
|
|
301
339
|
remove_listeners[key] = undefined;
|
|
302
340
|
}
|
|
303
341
|
}
|
|
304
342
|
|
|
305
343
|
for (const key in prev) {
|
|
306
|
-
if (!(key in next)) {
|
|
344
|
+
if (!(key in next) || is_ref_prop(next[key])) {
|
|
307
345
|
if (key === '#class') {
|
|
308
346
|
continue;
|
|
309
347
|
}
|
|
@@ -317,6 +355,9 @@ export function apply_element_spread(element, fn) {
|
|
|
317
355
|
if (key === 'children') continue;
|
|
318
356
|
|
|
319
357
|
let value = next[key];
|
|
358
|
+
if (is_ref_prop(value)) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
320
361
|
if (is_ripple_object(value)) {
|
|
321
362
|
value = get(value);
|
|
322
363
|
}
|
|
@@ -331,3 +372,18 @@ export function apply_element_spread(element, fn) {
|
|
|
331
372
|
prev = current;
|
|
332
373
|
};
|
|
333
374
|
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Keep spread refs in a branch block so ordinary spread updates do not destroy
|
|
378
|
+
* and recreate the ref block before `apply_element_spread` can compare the
|
|
379
|
+
* previous and current ref values.
|
|
380
|
+
*
|
|
381
|
+
* @param {Element} element
|
|
382
|
+
* @param {any} ref_fn
|
|
383
|
+
* @returns {Block}
|
|
384
|
+
*/
|
|
385
|
+
function create_spread_ref_effect(element, ref_fn) {
|
|
386
|
+
return branch(() => {
|
|
387
|
+
ref(element, () => ref_fn);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
REF_PROP,
|
|
30
30
|
TRACKED_OBJECT,
|
|
31
31
|
DEFAULT_NAMESPACE,
|
|
32
|
-
|
|
32
|
+
TRACKED_UPDATED,
|
|
33
33
|
SUSPENSE_PENDING,
|
|
34
34
|
SUSPENSE_REJECTED,
|
|
35
35
|
TRY_BLOCK,
|
|
@@ -44,18 +44,20 @@ import {
|
|
|
44
44
|
register_boundary_paused_block,
|
|
45
45
|
replace_boundary_request,
|
|
46
46
|
} from './try.js';
|
|
47
|
+
import { is_ripple_object } from './utils.js';
|
|
48
|
+
|
|
47
49
|
import {
|
|
48
50
|
define_property,
|
|
49
51
|
get_descriptor,
|
|
50
52
|
get_own_property_symbols,
|
|
51
53
|
is_array,
|
|
52
|
-
is_ripple_object,
|
|
53
54
|
object_keys,
|
|
54
|
-
} from '
|
|
55
|
+
} from '@tsrx/core/runtime/language-helpers';
|
|
55
56
|
import { get_async_track_result } from '../../../utils/async.js';
|
|
56
57
|
import { get_track_async_script_id } from '../../../utils/track-async-serialization.js';
|
|
57
58
|
import * as devalue from 'devalue';
|
|
58
59
|
import { hydrating, track_hash_reference } from './hydration.js';
|
|
60
|
+
import { create_ref_prop as create_core_ref_prop } from '@tsrx/core/runtime/ref';
|
|
59
61
|
|
|
60
62
|
const FLUSH_MICROTASK = 0;
|
|
61
63
|
const FLUSH_SYNC = 1;
|
|
@@ -668,7 +670,7 @@ export function track_async(fn, b, hash) {
|
|
|
668
670
|
|
|
669
671
|
// Abort previous in-flight request
|
|
670
672
|
if (abort_controller !== null && abort_controller.signal.aborted === false) {
|
|
671
|
-
abort_controller.abort(
|
|
673
|
+
abort_controller.abort(TRACKED_UPDATED);
|
|
672
674
|
}
|
|
673
675
|
abort_controller = null;
|
|
674
676
|
|
|
@@ -766,7 +768,7 @@ export function track_async(fn, b, hash) {
|
|
|
766
768
|
if (current_version !== version) return; // stale
|
|
767
769
|
|
|
768
770
|
var is_internal_abort =
|
|
769
|
-
error ===
|
|
771
|
+
error === TRACKED_UPDATED || current_abort_controller?.signal?.reason === TRACKED_UPDATED;
|
|
770
772
|
if (is_internal_abort) {
|
|
771
773
|
// Internal abort (superseded by a new request) — don't set rejected
|
|
772
774
|
if (request_id > 0 && boundary !== null) {
|
|
@@ -799,7 +801,7 @@ export function track_async(fn, b, hash) {
|
|
|
799
801
|
return () => {
|
|
800
802
|
// Teardown: abort in-flight request when block is destroyed
|
|
801
803
|
if (current_abort_controller !== null && current_abort_controller.signal.aborted === false) {
|
|
802
|
-
current_abort_controller.abort(
|
|
804
|
+
current_abort_controller.abort(TRACKED_UPDATED);
|
|
803
805
|
}
|
|
804
806
|
};
|
|
805
807
|
});
|
|
@@ -1653,6 +1655,15 @@ export function ref_prop() {
|
|
|
1653
1655
|
return Symbol(REF_PROP);
|
|
1654
1656
|
}
|
|
1655
1657
|
|
|
1658
|
+
/**
|
|
1659
|
+
* @param {() => any} get_ref_value
|
|
1660
|
+
* @param {(value: any) => void} [set_ref_value]
|
|
1661
|
+
* @returns {(node: any) => void | (() => void)}
|
|
1662
|
+
*/
|
|
1663
|
+
export function create_ref_prop(get_ref_value, set_ref_value) {
|
|
1664
|
+
return create_core_ref_prop(() => untrack(get_ref_value), set_ref_value);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1656
1667
|
/**
|
|
1657
1668
|
* @template T
|
|
1658
1669
|
* @param {T | undefined} value
|
|
@@ -9,15 +9,7 @@ import {
|
|
|
9
9
|
resume_block,
|
|
10
10
|
} from './blocks.js';
|
|
11
11
|
import { TRY_BLOCK } from './constants.js';
|
|
12
|
-
import {
|
|
13
|
-
hydrate_next,
|
|
14
|
-
hydrate_node,
|
|
15
|
-
hydrating,
|
|
16
|
-
set_hydrate_node,
|
|
17
|
-
set_hydrating,
|
|
18
|
-
skip_to_hydration_end,
|
|
19
|
-
} from './hydration.js';
|
|
20
|
-
import { get_next_sibling } from './operations.js';
|
|
12
|
+
import { hydrate_next, hydrating } from './hydration.js';
|
|
21
13
|
import {
|
|
22
14
|
active_block,
|
|
23
15
|
queue_microtask,
|
|
@@ -1,44 +1,5 @@
|
|
|
1
1
|
/** @import { NAMESPACE_URI } from './constants.js' */
|
|
2
2
|
|
|
3
|
-
/** @type {typeof Object.getOwnPropertyDescriptor} */
|
|
4
|
-
export var get_descriptor = Object.getOwnPropertyDescriptor;
|
|
5
|
-
/** @type {typeof Object.getOwnPropertyDescriptors} */
|
|
6
|
-
export var get_descriptors = Object.getOwnPropertyDescriptors;
|
|
7
|
-
/** @type {typeof Array.from} */
|
|
8
|
-
export var array_from = Array.from;
|
|
9
|
-
/** @type {typeof Array.isArray} */
|
|
10
|
-
export var is_array = Array.isArray;
|
|
11
|
-
/** @type {typeof Object.defineProperty} */
|
|
12
|
-
export var define_property = Object.defineProperty;
|
|
13
|
-
/** @type {typeof Object.getPrototypeOf} */
|
|
14
|
-
export var get_prototype_of = Object.getPrototypeOf;
|
|
15
|
-
/** @type {typeof Object.values} */
|
|
16
|
-
export var object_values = Object.values;
|
|
17
|
-
/** @type {typeof Object.entries} */
|
|
18
|
-
export var object_entries = Object.entries;
|
|
19
|
-
/** @type {typeof Object.keys} */
|
|
20
|
-
export var object_keys = Object.keys;
|
|
21
|
-
/** @type {typeof Object.getOwnPropertySymbols} */
|
|
22
|
-
export var get_own_property_symbols = Object.getOwnPropertySymbols;
|
|
23
|
-
/** @type {typeof structuredClone} */
|
|
24
|
-
export var structured_clone = structuredClone;
|
|
25
|
-
/** @type {typeof Object.prototype} */
|
|
26
|
-
export var object_prototype = Object.prototype;
|
|
27
|
-
/** @type {typeof Array.prototype} */
|
|
28
|
-
export var array_prototype = Array.prototype;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Slice helper for arrays and array-like values.
|
|
32
|
-
* @param {ArrayLike<any>} array_like
|
|
33
|
-
* @param {...number} args
|
|
34
|
-
* @returns {any[]}
|
|
35
|
-
*/
|
|
36
|
-
export function array_slice(array_like, ...args) {
|
|
37
|
-
return is_array(array_like)
|
|
38
|
-
? array_like.slice(...args)
|
|
39
|
-
: array_prototype.slice.call(array_like, ...args);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
3
|
/**
|
|
43
4
|
* Creates a text node that serves as an anchor point in the DOM.
|
|
44
5
|
* @returns {Text}
|
|
@@ -24,14 +24,16 @@ import {
|
|
|
24
24
|
SUSPENSE_PENDING,
|
|
25
25
|
SUSPENSE_REJECTED,
|
|
26
26
|
ASYNC_DERIVED_READ_THROWN,
|
|
27
|
-
|
|
27
|
+
TRACKED_UPDATED,
|
|
28
28
|
} from '../client/constants.js';
|
|
29
29
|
import { DEV } from 'esm-env';
|
|
30
|
-
import { is_ripple_object
|
|
30
|
+
import { is_ripple_object } from '../client/utils.js';
|
|
31
|
+
import { array_slice } from '@tsrx/core/runtime/language-helpers';
|
|
31
32
|
import { escape, escapeScript as escape_script } from '@tsrx/core';
|
|
32
33
|
import { isBooleanAttribute as is_boolean_attribute } from '@tsrx/core';
|
|
33
34
|
import { clsx } from 'clsx';
|
|
34
35
|
import { normalizeCssPropertyName as normalize_css_property_name } from '@tsrx/core';
|
|
36
|
+
import { create_ref_prop } from '@tsrx/core/runtime/ref';
|
|
35
37
|
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../constants.js';
|
|
36
38
|
import { is_tsrx_element, normalize_children, tsrx_element } from '../../element.js';
|
|
37
39
|
import {
|
|
@@ -56,6 +58,7 @@ export { context } from './context.js';
|
|
|
56
58
|
export { try_block, component_block, regular_block } from './blocks.js';
|
|
57
59
|
export { array_slice };
|
|
58
60
|
export { tsrx_element, normalize_children };
|
|
61
|
+
export { create_ref_prop };
|
|
59
62
|
|
|
60
63
|
/** @extends Error */
|
|
61
64
|
export class TrackAsyncRunError extends Error {
|
|
@@ -1566,7 +1569,7 @@ function register_block_rerun(block) {
|
|
|
1566
1569
|
cancel: () => {
|
|
1567
1570
|
cancelled = true;
|
|
1568
1571
|
if (t && t.aa) {
|
|
1569
|
-
t.aa.abort(
|
|
1572
|
+
t.aa.abort(TRACKED_UPDATED);
|
|
1570
1573
|
t.aa = null;
|
|
1571
1574
|
t.ap = null;
|
|
1572
1575
|
}
|
package/src/runtime/proxy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Tracked } from 'ripple';
|
|
2
|
-
import {
|
|
2
|
+
import { TRACKED_UPDATED, effect, flushSync, track, trackAsync } from 'ripple';
|
|
3
3
|
|
|
4
4
|
describe('async suspense', () => {
|
|
5
5
|
it('hides child content during re-suspension when tracked dependency changes', async () => {
|
|
@@ -115,7 +115,7 @@ describe('async suspense', () => {
|
|
|
115
115
|
expect(container.innerHTML).not.toContain('late value');
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
it('aborts superseded requests with
|
|
118
|
+
it('aborts superseded requests with TRACKED_UPDATED without rendering catch', async () => {
|
|
119
119
|
const requests = new Map<
|
|
120
120
|
string,
|
|
121
121
|
{ resolve: (value: string) => void; abortController: AbortController }
|
|
@@ -181,7 +181,7 @@ describe('async suspense', () => {
|
|
|
181
181
|
|
|
182
182
|
expect(
|
|
183
183
|
(requests.get('b') as { abortController: AbortController }).abortController.signal.reason,
|
|
184
|
-
).toBe(
|
|
184
|
+
).toBe(TRACKED_UPDATED);
|
|
185
185
|
expect(container.innerHTML).not.toContain('class="error"');
|
|
186
186
|
expect(container.innerHTML).toContain('value-a');
|
|
187
187
|
|
|
@@ -123,6 +123,36 @@ describe('basic client > events', () => {
|
|
|
123
123
|
expect(countSpan.textContent).toBe('1');
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
it('removes event listeners from spread props when they are replaced by another prop', () => {
|
|
127
|
+
component Basic() {
|
|
128
|
+
let &[enabled] = track(true);
|
|
129
|
+
let &[count] = track(0);
|
|
130
|
+
|
|
131
|
+
<button class="target" {...(enabled ? { onClick: () => count++ } : { title: 'disabled' })}>
|
|
132
|
+
{'target'}
|
|
133
|
+
</button>
|
|
134
|
+
<button class="toggle" onClick={() => (enabled = false)}>{'toggle'}</button>
|
|
135
|
+
<span class="count">{count}</span>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
render(Basic);
|
|
139
|
+
|
|
140
|
+
const target = container.querySelector('.target');
|
|
141
|
+
const toggle = container.querySelector('.toggle');
|
|
142
|
+
const count = container.querySelector('.count');
|
|
143
|
+
|
|
144
|
+
target.click();
|
|
145
|
+
flushSync();
|
|
146
|
+
expect(count.textContent).toBe('1');
|
|
147
|
+
|
|
148
|
+
toggle.click();
|
|
149
|
+
flushSync();
|
|
150
|
+
expect(target.getAttribute('title')).toBe('disabled');
|
|
151
|
+
target.click();
|
|
152
|
+
flushSync();
|
|
153
|
+
expect(count.textContent).toBe('1');
|
|
154
|
+
});
|
|
155
|
+
|
|
126
156
|
it('handles both delegated and non-delegated events in spread props', () => {
|
|
127
157
|
component Basic() {
|
|
128
158
|
let &[clickCount] = track(0);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PropsWithExtras } from 'ripple';
|
|
2
|
-
import { createRefKey, flushSync, track } from 'ripple';
|
|
2
|
+
import { createRefKey, effect, flushSync, track } from 'ripple';
|
|
3
3
|
|
|
4
4
|
describe('dynamic DOM elements', () => {
|
|
5
5
|
it('renders static dynamic element', () => {
|
|
@@ -225,6 +225,84 @@ describe('dynamic DOM elements', () => {
|
|
|
225
225
|
expect(capturedElement!.textContent).toBe('Element with ref');
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
it('handles ref={...}, {ref ...}, and named ref props on dynamic DOM elements', () => {
|
|
229
|
+
let refAttrElement: HTMLInputElement | null = null;
|
|
230
|
+
let anonymousRefElement: HTMLInputElement | null = null;
|
|
231
|
+
let namedRefElement: HTMLInputElement | null = null;
|
|
232
|
+
|
|
233
|
+
component App() {
|
|
234
|
+
let tag = track('input');
|
|
235
|
+
let input: HTMLInputElement | undefined;
|
|
236
|
+
const state: { anonymous?: HTMLInputElement } = {};
|
|
237
|
+
|
|
238
|
+
<@tag
|
|
239
|
+
id="dynamic-ref-combo"
|
|
240
|
+
type="text"
|
|
241
|
+
ref={input}
|
|
242
|
+
{ref state.anonymous}
|
|
243
|
+
input_ref={ref (node: HTMLInputElement | null) => {
|
|
244
|
+
namedRefElement = node;
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
247
|
+
|
|
248
|
+
effect(() => {
|
|
249
|
+
refAttrElement = input ?? null;
|
|
250
|
+
anonymousRefElement = state.anonymous ?? null;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
render(App);
|
|
255
|
+
flushSync();
|
|
256
|
+
|
|
257
|
+
const element = container.querySelector('#dynamic-ref-combo');
|
|
258
|
+
expect(element).toBeInstanceOf(HTMLInputElement);
|
|
259
|
+
expect(refAttrElement).toBe(element);
|
|
260
|
+
expect(anonymousRefElement).toBe(element);
|
|
261
|
+
expect(namedRefElement).toBe(element);
|
|
262
|
+
expect(element!.hasAttribute('ref')).toBe(false);
|
|
263
|
+
expect(element!.hasAttribute('input_ref')).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('forwards dynamic ref forms through a dynamic component that spreads props', () => {
|
|
267
|
+
let refAttrElement: HTMLInputElement | null = null;
|
|
268
|
+
let anonymousRefElement: HTMLInputElement | null = null;
|
|
269
|
+
let namedRefElement: HTMLInputElement | null = null;
|
|
270
|
+
|
|
271
|
+
component Child(props: PropsWithExtras<{}>) {
|
|
272
|
+
<input id="dynamic-component-ref-combo" type="text" {...props} />
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
component App() {
|
|
276
|
+
let dynamic = track(() => Child);
|
|
277
|
+
let input: HTMLInputElement | undefined;
|
|
278
|
+
const state: { anonymous?: HTMLInputElement } = {};
|
|
279
|
+
|
|
280
|
+
<@dynamic
|
|
281
|
+
ref={input}
|
|
282
|
+
{ref state.anonymous}
|
|
283
|
+
input_ref={ref (node: HTMLInputElement | null) => {
|
|
284
|
+
namedRefElement = node;
|
|
285
|
+
}}
|
|
286
|
+
/>
|
|
287
|
+
|
|
288
|
+
effect(() => {
|
|
289
|
+
refAttrElement = input ?? null;
|
|
290
|
+
anonymousRefElement = state.anonymous ?? null;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
render(App);
|
|
295
|
+
flushSync();
|
|
296
|
+
|
|
297
|
+
const element = container.querySelector('#dynamic-component-ref-combo');
|
|
298
|
+
expect(element).toBeInstanceOf(HTMLInputElement);
|
|
299
|
+
expect(refAttrElement).toBe(element);
|
|
300
|
+
expect(anonymousRefElement).toBe(element);
|
|
301
|
+
expect(namedRefElement).toBe(element);
|
|
302
|
+
expect(element!.hasAttribute('ref')).toBe(false);
|
|
303
|
+
expect(element!.hasAttribute('input_ref')).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
|
|
228
306
|
it('handles dynamic element with createRefKey in spread', () => {
|
|
229
307
|
component App() {
|
|
230
308
|
let tag = track('header');
|
|
@@ -1,9 +1,70 @@
|
|
|
1
1
|
import type { PropsWithExtras } from 'ripple';
|
|
2
2
|
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { RippleArray, createRefKey, effect, flushSync, track } from 'ripple';
|
|
3
|
+
import { RippleArray, createRefKey, effect, flushSync, isRefProp, track } from 'ripple';
|
|
4
4
|
import type { Tracked } from 'ripple';
|
|
5
5
|
|
|
6
6
|
describe('refs', () => {
|
|
7
|
+
it('reports ordinary functions and ref objects as non named-ref props', () => {
|
|
8
|
+
expect(isRefProp(() => {})).toBe(false);
|
|
9
|
+
expect(isRefProp({ current: null })).toBe(false);
|
|
10
|
+
expect(isRefProp({ value: null })).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('captures a host element with a named ref prop', () => {
|
|
14
|
+
let captured: HTMLInputElement | null = null;
|
|
15
|
+
|
|
16
|
+
component App() {
|
|
17
|
+
let input: HTMLInputElement | undefined;
|
|
18
|
+
|
|
19
|
+
<input type="text" input_ref={ref input} />
|
|
20
|
+
|
|
21
|
+
effect(() => {
|
|
22
|
+
captured = input ?? null;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render(App);
|
|
27
|
+
flushSync();
|
|
28
|
+
|
|
29
|
+
expect(captured).toBeInstanceOf(HTMLInputElement);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('forwards a named ref prop explicitly through a component', () => {
|
|
33
|
+
let captured: HTMLInputElement | null = null;
|
|
34
|
+
|
|
35
|
+
component Child(props: PropsWithExtras<{}>) {
|
|
36
|
+
expect(isRefProp(props.input_ref)).toBe(true);
|
|
37
|
+
<input type="text" ref={props.input_ref} />
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
component App() {
|
|
41
|
+
<Child input_ref={ref (node: HTMLInputElement | null) => (captured = node)} />
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
render(App);
|
|
45
|
+
flushSync();
|
|
46
|
+
|
|
47
|
+
expect(captured).toBeInstanceOf(HTMLInputElement);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('applies named ref props from host spreads', () => {
|
|
51
|
+
let captured: HTMLInputElement | null = null;
|
|
52
|
+
|
|
53
|
+
component Child(props: PropsWithExtras<{}>) {
|
|
54
|
+
expect(isRefProp(props.input_ref)).toBe(true);
|
|
55
|
+
<input type="text" {...props} />
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
component App() {
|
|
59
|
+
<Child input_ref={ref (node: HTMLInputElement | null) => (captured = node)} />
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
render(App);
|
|
63
|
+
flushSync();
|
|
64
|
+
|
|
65
|
+
expect(captured).toBeInstanceOf(HTMLInputElement);
|
|
66
|
+
});
|
|
67
|
+
|
|
7
68
|
it('capture a <div>', () => {
|
|
8
69
|
let div: HTMLDivElement | undefined;
|
|
9
70
|
|
|
@@ -138,6 +199,95 @@ describe('refs', () => {
|
|
|
138
199
|
expect(captured!.textContent).toBe('Hello World');
|
|
139
200
|
});
|
|
140
201
|
|
|
202
|
+
it('assigns a host element to a plain let variable via ref={var}', () => {
|
|
203
|
+
let captured: HTMLDivElement | null = null;
|
|
204
|
+
|
|
205
|
+
component App() {
|
|
206
|
+
let div: HTMLDivElement | undefined;
|
|
207
|
+
|
|
208
|
+
<div ref={div}>{'Hello ref attr'}</div>
|
|
209
|
+
|
|
210
|
+
effect(() => {
|
|
211
|
+
captured = div ?? null;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
render(App);
|
|
216
|
+
flushSync();
|
|
217
|
+
expect(captured).toBeInstanceOf(HTMLDivElement);
|
|
218
|
+
expect(captured!.textContent).toBe('Hello ref attr');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('clears a plain let variable via ref={var} when the host element unmounts', () => {
|
|
222
|
+
let div: HTMLDivElement | null | undefined;
|
|
223
|
+
|
|
224
|
+
component App() {
|
|
225
|
+
let &[show] = track(true);
|
|
226
|
+
|
|
227
|
+
if (show) {
|
|
228
|
+
<div ref={div}>{'Hello cleanup'}</div>
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
<button class="toggle" onClick={() => (show = false)}>{'hide'}</button>
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
render(App);
|
|
235
|
+
flushSync();
|
|
236
|
+
expect(div).toBeInstanceOf(HTMLDivElement);
|
|
237
|
+
|
|
238
|
+
(container.querySelector('.toggle') as HTMLButtonElement).click();
|
|
239
|
+
flushSync();
|
|
240
|
+
expect(div).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('assigns a host element to a member expression via ref={state.var}', () => {
|
|
244
|
+
let captured: HTMLInputElement | null = null;
|
|
245
|
+
|
|
246
|
+
component App() {
|
|
247
|
+
const state: { input?: HTMLInputElement } = {};
|
|
248
|
+
|
|
249
|
+
<input type="text" ref={state.input} />
|
|
250
|
+
|
|
251
|
+
effect(() => {
|
|
252
|
+
captured = state.input ?? null;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
render(App);
|
|
257
|
+
flushSync();
|
|
258
|
+
expect(captured).toBeInstanceOf(HTMLInputElement);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('clears a plain let variable via component ref={var} when the host element unmounts', () => {
|
|
262
|
+
let input: HTMLInputElement | null | undefined;
|
|
263
|
+
let previous: HTMLInputElement | undefined;
|
|
264
|
+
|
|
265
|
+
component Child(props: PropsWithExtras<{}>) {
|
|
266
|
+
<input type="text" value="keep" {...props} />
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
component App() {
|
|
270
|
+
let &[show] = track(true);
|
|
271
|
+
|
|
272
|
+
if (show) {
|
|
273
|
+
<Child ref={input} />
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
<button class="toggle" onClick={() => (show = false)}>{'hide'}</button>
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
render(App);
|
|
280
|
+
flushSync();
|
|
281
|
+
expect(input).toBeInstanceOf(HTMLInputElement);
|
|
282
|
+
previous = input!;
|
|
283
|
+
expect(previous.value).toBe('keep');
|
|
284
|
+
|
|
285
|
+
(container.querySelector('.toggle') as HTMLButtonElement).click();
|
|
286
|
+
flushSync();
|
|
287
|
+
expect(input).toBeNull();
|
|
288
|
+
expect(previous.value).toBe('keep');
|
|
289
|
+
});
|
|
290
|
+
|
|
141
291
|
it(
|
|
142
292
|
'uses the function path even when the variable is an Identifier (function wins over setter)',
|
|
143
293
|
() => {
|
|
@@ -176,14 +326,7 @@ describe('refs', () => {
|
|
|
176
326
|
},
|
|
177
327
|
);
|
|
178
328
|
|
|
179
|
-
it('
|
|
180
|
-
// Assignment-sugar (`let foo; <el {ref foo} />`) only works on
|
|
181
|
-
// host elements, where the setter closure has direct lexical
|
|
182
|
-
// access to the parent's slot. Passing a plain `let` variable
|
|
183
|
-
// through a composite forwards only its current value into the
|
|
184
|
-
// child's local prop bag — there is no slot identity across the
|
|
185
|
-
// component boundary. Use a `Tracked` (object identity) when you
|
|
186
|
-
// need the captured node to be visible in the parent.
|
|
329
|
+
it('propagates a plain let variable through a composite component via {...rest}', () => {
|
|
187
330
|
let captured: HTMLInputElement | null = null;
|
|
188
331
|
|
|
189
332
|
component Child({ id, ...rest }: PropsWithExtras<{ id: string }>) {
|
|
@@ -202,11 +345,108 @@ describe('refs', () => {
|
|
|
202
345
|
|
|
203
346
|
render(App);
|
|
204
347
|
flushSync();
|
|
205
|
-
// The DOM input was created and exists — but the parent's `input`
|
|
206
|
-
// slot stays unset because there is no setter to forward through
|
|
207
|
-
// the composite boundary.
|
|
208
348
|
expect(container.querySelector('input')).toBeInstanceOf(HTMLInputElement);
|
|
209
|
-
expect(captured).
|
|
349
|
+
expect(captured).toBeInstanceOf(HTMLInputElement);
|
|
350
|
+
expect(captured!.id).toBe('example');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it(
|
|
354
|
+
'clears a plain let variable forwarded through a named ref prop when the host element unmounts',
|
|
355
|
+
() => {
|
|
356
|
+
let input: HTMLInputElement | null | undefined;
|
|
357
|
+
let previous: HTMLInputElement | undefined;
|
|
358
|
+
|
|
359
|
+
component Child(props: PropsWithExtras<{}>) {
|
|
360
|
+
<input type="text" value="keep" {...props} />
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
component App() {
|
|
364
|
+
let &[show] = track(true);
|
|
365
|
+
|
|
366
|
+
if (show) {
|
|
367
|
+
<Child input_ref={ref input} />
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
<button class="toggle" onClick={() => (show = false)}>{'hide'}</button>
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
render(App);
|
|
374
|
+
flushSync();
|
|
375
|
+
expect(input).toBeInstanceOf(HTMLInputElement);
|
|
376
|
+
previous = input!;
|
|
377
|
+
expect(previous.value).toBe('keep');
|
|
378
|
+
|
|
379
|
+
(container.querySelector('.toggle') as HTMLButtonElement).click();
|
|
380
|
+
flushSync();
|
|
381
|
+
expect(input).toBeNull();
|
|
382
|
+
expect(previous.value).toBe('keep');
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
it('clears a named ref prop when a host spread changes it to a regular prop', () => {
|
|
387
|
+
let input: HTMLInputElement | null | undefined;
|
|
388
|
+
let previous: HTMLInputElement | undefined;
|
|
389
|
+
|
|
390
|
+
component Child(props: PropsWithExtras<{}>) {
|
|
391
|
+
let &[as_ref] = track(true);
|
|
392
|
+
|
|
393
|
+
<input
|
|
394
|
+
type="text"
|
|
395
|
+
value="keep"
|
|
396
|
+
{...(as_ref ? { input_ref: props.input_ref } : { input_ref: 'regular prop' })}
|
|
397
|
+
/>
|
|
398
|
+
<button class="toggle" onClick={() => (as_ref = false)}>{'toggle'}</button>
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
component App() {
|
|
402
|
+
<Child input_ref={ref input} />
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
render(App);
|
|
406
|
+
flushSync();
|
|
407
|
+
expect(input).toBeInstanceOf(HTMLInputElement);
|
|
408
|
+
previous = input!;
|
|
409
|
+
expect(previous.value).toBe('keep');
|
|
410
|
+
expect(previous.getAttribute('input_ref')).toBeNull();
|
|
411
|
+
|
|
412
|
+
(container.querySelector('.toggle') as HTMLButtonElement).click();
|
|
413
|
+
flushSync();
|
|
414
|
+
expect(input).toBeNull();
|
|
415
|
+
expect(previous.value).toBe('keep');
|
|
416
|
+
expect(previous.getAttribute('input_ref')).toBe('regular prop');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('removes a regular spread attribute when the key changes to a named ref prop', () => {
|
|
420
|
+
let input: HTMLInputElement | null | undefined;
|
|
421
|
+
let previous: HTMLInputElement | undefined;
|
|
422
|
+
|
|
423
|
+
component Child(props: PropsWithExtras<{}>) {
|
|
424
|
+
let &[as_ref] = track(false);
|
|
425
|
+
|
|
426
|
+
<input
|
|
427
|
+
type="text"
|
|
428
|
+
value="keep"
|
|
429
|
+
{...(as_ref ? { input_ref: props.input_ref } : { input_ref: 'regular prop' })}
|
|
430
|
+
/>
|
|
431
|
+
<button class="toggle" onClick={() => (as_ref = true)}>{'toggle'}</button>
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
component App() {
|
|
435
|
+
<Child input_ref={ref input} />
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
render(App);
|
|
439
|
+
flushSync();
|
|
440
|
+
expect(input).toBeUndefined();
|
|
441
|
+
previous = container.querySelector('input') as HTMLInputElement;
|
|
442
|
+
expect(previous.value).toBe('keep');
|
|
443
|
+
expect(previous.getAttribute('input_ref')).toBe('regular prop');
|
|
444
|
+
|
|
445
|
+
(container.querySelector('.toggle') as HTMLButtonElement).click();
|
|
446
|
+
flushSync();
|
|
447
|
+
expect(input).toBe(previous);
|
|
448
|
+
expect(previous.value).toBe('keep');
|
|
449
|
+
expect(previous.getAttribute('input_ref')).toBeNull();
|
|
210
450
|
});
|
|
211
451
|
|
|
212
452
|
it('clears the Tracked when the host element unmounts', () => {
|
package/types/index.d.ts
CHANGED
|
@@ -146,8 +146,10 @@ declare global {
|
|
|
146
146
|
|
|
147
147
|
export function createRefKey(): symbol;
|
|
148
148
|
|
|
149
|
+
export function isRefProp(value: unknown): boolean;
|
|
150
|
+
|
|
149
151
|
export const UNINITIALIZED: unique symbol;
|
|
150
|
-
export const
|
|
152
|
+
export const TRACKED_UPDATED: unique symbol;
|
|
151
153
|
export const SUSPENSE_PENDING: unique symbol;
|
|
152
154
|
export const SUSPENSE_REJECTED: unique symbol;
|
|
153
155
|
|