ripple 0.3.37 → 0.3.39
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 +23 -0
- package/package.json +4 -4
- package/src/runtime/internal/client/blocks.js +37 -7
- package/tests/client/ref.test.tsrx +157 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# ripple
|
|
2
2
|
|
|
3
|
+
## 0.3.39
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies []:
|
|
8
|
+
- ripple@0.3.39
|
|
9
|
+
- @tsrx/ripple@0.0.21
|
|
10
|
+
|
|
11
|
+
## 0.3.38
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#1007](https://github.com/Ripple-TS/ripple/pull/1007)
|
|
16
|
+
[`088299c`](https://github.com/Ripple-TS/ripple/commit/088299ce94a6022c017ce2e56c7e1b59bd5973f7)
|
|
17
|
+
Thanks [@trueadm](https://github.com/trueadm)! - Keep double-quoted JavaScript
|
|
18
|
+
strings inside TSRX expression containers using normal JavaScript string
|
|
19
|
+
semantics while preserving direct double-quoted text child parsing.
|
|
20
|
+
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
[[`088299c`](https://github.com/Ripple-TS/ripple/commit/088299ce94a6022c017ce2e56c7e1b59bd5973f7)]:
|
|
23
|
+
- @tsrx/ripple@0.0.20
|
|
24
|
+
- ripple@0.3.38
|
|
25
|
+
|
|
3
26
|
## 0.3.37
|
|
4
27
|
|
|
5
28
|
### 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.39",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"esm-env": "^1.2.2",
|
|
77
77
|
"@types/estree": "^1.0.8",
|
|
78
78
|
"@types/estree-jsx": "^1.0.5",
|
|
79
|
-
"@tsrx/ripple": "0.0.
|
|
79
|
+
"@tsrx/ripple": "0.0.21"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@types/node": "^24.3.0",
|
|
@@ -84,9 +84,9 @@
|
|
|
84
84
|
"typescript": "^5.9.3",
|
|
85
85
|
"@volar/language-core": "~2.4.28",
|
|
86
86
|
"vscode-languageserver-types": "^3.17.5",
|
|
87
|
-
"@tsrx/core": "0.0.
|
|
87
|
+
"@tsrx/core": "0.0.19"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
|
-
"ripple": "0.3.
|
|
90
|
+
"ripple": "0.3.39"
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
TRY_BLOCK,
|
|
15
15
|
HEAD_BLOCK,
|
|
16
16
|
DIRECT_CHILD_BLOCK,
|
|
17
|
+
UNINITIALIZED,
|
|
17
18
|
} from './constants.js';
|
|
18
19
|
import { next_sibling } from './operations.js';
|
|
19
20
|
import { apply_element_spread } from './render.js';
|
|
@@ -26,7 +27,9 @@ import {
|
|
|
26
27
|
run_block,
|
|
27
28
|
run_teardown,
|
|
28
29
|
schedule_update,
|
|
30
|
+
untrack,
|
|
29
31
|
} from './runtime.js';
|
|
32
|
+
import { is_ripple_object } from './utils.js';
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* @param {Function} fn
|
|
@@ -96,29 +99,56 @@ export function branch(fn, flags = 0, state = null) {
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
/**
|
|
102
|
+
* Wire up a `{ref expr}` attribute. `expr` may be:
|
|
103
|
+
* - a callback function — invoked with the element on mount; if it returns
|
|
104
|
+
* a function, that function runs as the cleanup on unmount.
|
|
105
|
+
* - a `Tracked` (e.g. from `track()`) — `tracked.value` is set to the
|
|
106
|
+
* element on mount and reset to `null` on unmount.
|
|
107
|
+
* - a plain mutable var (`let foo;`) — the element is assigned to the
|
|
108
|
+
* variable. No teardown is run, released with the component.
|
|
109
|
+
*
|
|
110
|
+
* `get_fn` is invoked through `untrack` so the surrounding render block
|
|
111
|
+
* doesn't subscribe to whatever the thunk happens to read. The supported
|
|
112
|
+
* shape is to pass the ref slot itself (`{ref tracker}`); a foot-gun like
|
|
113
|
+
* `{ref tracker.value}` would otherwise read the cell reactively and cause
|
|
114
|
+
* spurious re-runs.
|
|
115
|
+
*
|
|
99
116
|
* @param {Element} element
|
|
100
|
-
* @param {() =>
|
|
117
|
+
* @param {() => any} get_fn
|
|
118
|
+
* @param {(value: any) => void} [set_fn]
|
|
101
119
|
* @returns {Block}
|
|
102
120
|
*/
|
|
103
|
-
export function ref(element, get_fn) {
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
export function ref(element, get_fn, set_fn) {
|
|
122
|
+
// make sure the first run always enters the dispatch branch,
|
|
123
|
+
/** @type {any} */
|
|
124
|
+
var ref_value = UNINITIALIZED;
|
|
106
125
|
/** @type {Block | null} */
|
|
107
126
|
var e;
|
|
108
127
|
|
|
109
128
|
return block(RENDER_BLOCK, () => {
|
|
110
|
-
|
|
129
|
+
// avoid any reactive reads
|
|
130
|
+
var next = untrack(get_fn);
|
|
131
|
+
if (ref_value !== (ref_value = next)) {
|
|
111
132
|
if (e) {
|
|
112
133
|
destroy_block(e);
|
|
113
134
|
e = null;
|
|
114
135
|
}
|
|
115
136
|
|
|
116
|
-
if (
|
|
137
|
+
if (typeof ref_value === 'function') {
|
|
138
|
+
e = branch(() => {
|
|
139
|
+
effect(() => ref_value(element));
|
|
140
|
+
});
|
|
141
|
+
} else if (is_ripple_object(ref_value)) {
|
|
117
142
|
e = branch(() => {
|
|
118
143
|
effect(() => {
|
|
119
|
-
|
|
144
|
+
ref_value.value = element;
|
|
145
|
+
return () => {
|
|
146
|
+
ref_value.value = null;
|
|
147
|
+
};
|
|
120
148
|
});
|
|
121
149
|
});
|
|
150
|
+
} else if (set_fn !== undefined) {
|
|
151
|
+
set_fn(element);
|
|
122
152
|
}
|
|
123
153
|
}
|
|
124
154
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PropsWithExtras } from 'ripple';
|
|
2
2
|
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { RippleArray, createRefKey, flushSync, track } from 'ripple';
|
|
3
|
+
import { RippleArray, createRefKey, effect, flushSync, track } from 'ripple';
|
|
4
|
+
import type { Tracked } from 'ripple';
|
|
4
5
|
|
|
5
6
|
describe('refs', () => {
|
|
6
7
|
it('capture a <div>', () => {
|
|
@@ -77,6 +78,161 @@ describe('refs', () => {
|
|
|
77
78
|
expect(logs).toEqual(['ref called', 'ref called']);
|
|
78
79
|
});
|
|
79
80
|
|
|
81
|
+
it('captures a host element into a Tracked via {ref tracker}', () => {
|
|
82
|
+
let captured: Tracked<HTMLDivElement | null> | undefined;
|
|
83
|
+
|
|
84
|
+
component Component() {
|
|
85
|
+
const tracker = track<HTMLDivElement | null>(null);
|
|
86
|
+
captured = tracker;
|
|
87
|
+
|
|
88
|
+
<div {ref tracker}>{'Hello World'}</div>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
render(Component);
|
|
92
|
+
flushSync();
|
|
93
|
+
expect(captured!.value).toBeInstanceOf(HTMLDivElement);
|
|
94
|
+
expect(captured!.value!.textContent).toBe('Hello World');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('forwards a Tracked through a composite component via prop destructuring + spread', () => {
|
|
98
|
+
let captured: Tracked<HTMLInputElement | null> | undefined;
|
|
99
|
+
|
|
100
|
+
component Child({ id, ...rest }: PropsWithExtras<{ id: string }>) {
|
|
101
|
+
// Symbol-keyed `ref` prop survives `...rest` destructuring and
|
|
102
|
+
// arrives on the DOM element via spread.
|
|
103
|
+
<input type="text" {id} {...rest} />
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
component App() {
|
|
107
|
+
const tracker = track<HTMLInputElement | null>(null);
|
|
108
|
+
captured = tracker;
|
|
109
|
+
|
|
110
|
+
<Child id="example" {ref tracker} />
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
render(App);
|
|
114
|
+
flushSync();
|
|
115
|
+
expect(captured!.value).toBeInstanceOf(HTMLInputElement);
|
|
116
|
+
expect(captured!.value!.id).toBe('example');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('assigns a host element to a plain let variable via {ref var}', () => {
|
|
120
|
+
let captured: HTMLDivElement | null = null;
|
|
121
|
+
|
|
122
|
+
component App() {
|
|
123
|
+
let div: HTMLDivElement | undefined;
|
|
124
|
+
|
|
125
|
+
<div {ref div}>{'Hello World'}</div>
|
|
126
|
+
|
|
127
|
+
// Read the captured element through an effect so the assertion
|
|
128
|
+
// observes the post-mount value (component setup runs before the
|
|
129
|
+
// element is created).
|
|
130
|
+
effect(() => {
|
|
131
|
+
captured = div ?? null;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
render(App);
|
|
136
|
+
flushSync();
|
|
137
|
+
expect(captured).toBeInstanceOf(HTMLDivElement);
|
|
138
|
+
expect(captured!.textContent).toBe('Hello World');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it(
|
|
142
|
+
'uses the function path even when the variable is an Identifier (function wins over setter)',
|
|
143
|
+
() => {
|
|
144
|
+
let logs: string[] = [];
|
|
145
|
+
|
|
146
|
+
component App() {
|
|
147
|
+
let cb = (node: HTMLDivElement) => {
|
|
148
|
+
logs.push(`mount:${node.textContent}`);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
<div {ref cb}>{'Hello'}</div>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
render(App);
|
|
155
|
+
flushSync();
|
|
156
|
+
expect(logs).toEqual(['mount:Hello']);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
it(
|
|
161
|
+
'uses the Tracked path even when the variable is an Identifier (Tracked wins over setter)',
|
|
162
|
+
() => {
|
|
163
|
+
let captured: Tracked<HTMLDivElement | null> | undefined;
|
|
164
|
+
|
|
165
|
+
component App() {
|
|
166
|
+
const tracker = track<HTMLDivElement | null>(null);
|
|
167
|
+
let slot = tracker;
|
|
168
|
+
captured = tracker;
|
|
169
|
+
|
|
170
|
+
<div {ref slot}>{'Hello'}</div>
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
render(App);
|
|
174
|
+
flushSync();
|
|
175
|
+
expect(captured!.value).toBeInstanceOf(HTMLDivElement);
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
it('does NOT propagate a plain let variable through a composite component via {...rest}', () => {
|
|
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.
|
|
187
|
+
let captured: HTMLInputElement | null = null;
|
|
188
|
+
|
|
189
|
+
component Child({ id, ...rest }: PropsWithExtras<{ id: string }>) {
|
|
190
|
+
<input type="text" {id} {...rest} />
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
component App() {
|
|
194
|
+
let input: HTMLInputElement | undefined;
|
|
195
|
+
|
|
196
|
+
<Child id="example" {ref input} />
|
|
197
|
+
|
|
198
|
+
effect(() => {
|
|
199
|
+
captured = input ?? null;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
render(App);
|
|
204
|
+
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
|
+
expect(container.querySelector('input')).toBeInstanceOf(HTMLInputElement);
|
|
209
|
+
expect(captured).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('clears the Tracked when the host element unmounts', () => {
|
|
213
|
+
let captured: Tracked<HTMLDivElement | null> | undefined;
|
|
214
|
+
let toggle: Tracked<boolean> | undefined;
|
|
215
|
+
|
|
216
|
+
component Component() {
|
|
217
|
+
const tracker = track<HTMLDivElement | null>(null);
|
|
218
|
+
const show = track(true);
|
|
219
|
+
captured = tracker;
|
|
220
|
+
toggle = show;
|
|
221
|
+
|
|
222
|
+
if (show.value) {
|
|
223
|
+
<div {ref tracker}>{'Hello World'}</div>
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
render(Component);
|
|
228
|
+
flushSync();
|
|
229
|
+
expect(captured!.value).toBeInstanceOf(HTMLDivElement);
|
|
230
|
+
|
|
231
|
+
toggle!.value = false;
|
|
232
|
+
flushSync();
|
|
233
|
+
expect(captured!.value).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
80
236
|
it('should handle spreading props with a static ref', () => {
|
|
81
237
|
let logs: string[] = [];
|
|
82
238
|
|