ripple 0.2.115 → 0.2.116
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/package.json +1 -1
- package/src/compiler/phases/1-parse/index.js +79 -0
- package/src/compiler/phases/3-transform/client/index.js +54 -8
- package/src/compiler/phases/3-transform/segments.js +15 -10
- package/src/compiler/types/index.d.ts +16 -0
- package/src/runtime/index-client.js +18 -184
- package/src/runtime/internal/client/bindings.js +443 -0
- package/src/runtime/internal/client/index.js +4 -0
- package/src/runtime/map.js +11 -1
- package/src/runtime/set.js +11 -1
- package/tests/client/map.test.ripple +81 -0
- package/tests/client/set.test.ripple +81 -0
- package/types/index.d.ts +87 -45
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/** @import { Block, Tracked } from '#client' */
|
|
2
|
+
|
|
3
|
+
import { effect, render } from './blocks.js';
|
|
4
|
+
import { on } from './events.js';
|
|
5
|
+
import { active_block, get, set, tick, untrack } from './runtime.js';
|
|
6
|
+
import { is_array, is_tracked_object } from './utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resize observer singleton.
|
|
10
|
+
* One listener per element only!
|
|
11
|
+
* https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ
|
|
12
|
+
*/
|
|
13
|
+
class ResizeObserverSingleton {
|
|
14
|
+
/** */
|
|
15
|
+
#listeners = new WeakMap();
|
|
16
|
+
|
|
17
|
+
/** @type {ResizeObserver | undefined} */
|
|
18
|
+
#observer;
|
|
19
|
+
|
|
20
|
+
/** @type {ResizeObserverOptions} */
|
|
21
|
+
#options;
|
|
22
|
+
|
|
23
|
+
/** @static */
|
|
24
|
+
static entries = new WeakMap();
|
|
25
|
+
|
|
26
|
+
/** @param {ResizeObserverOptions} options */
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.#options = options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {Element} element
|
|
33
|
+
* @param {(entry: ResizeObserverEntry) => any} listener
|
|
34
|
+
*/
|
|
35
|
+
observe(element, listener) {
|
|
36
|
+
var listeners = this.#listeners.get(element) || new Set();
|
|
37
|
+
listeners.add(listener);
|
|
38
|
+
|
|
39
|
+
this.#listeners.set(element, listeners);
|
|
40
|
+
this.#getObserver().observe(element, this.#options);
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
var listeners = this.#listeners.get(element);
|
|
44
|
+
listeners.delete(listener);
|
|
45
|
+
|
|
46
|
+
if (listeners.size === 0) {
|
|
47
|
+
this.#listeners.delete(element);
|
|
48
|
+
/** @type {ResizeObserver} */ (this.#observer).unobserve(element);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#getObserver() {
|
|
54
|
+
return (
|
|
55
|
+
this.#observer ??
|
|
56
|
+
(this.#observer = new ResizeObserver(
|
|
57
|
+
/** @param {any} entries */ (entries) => {
|
|
58
|
+
for (var entry of entries) {
|
|
59
|
+
ResizeObserverSingleton.entries.set(entry.target, entry);
|
|
60
|
+
for (var listener of this.#listeners.get(entry.target) || []) {
|
|
61
|
+
listener(entry);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
))
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var resize_observer_content_box = /* @__PURE__ */ new ResizeObserverSingleton({
|
|
71
|
+
box: 'content-box',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
var resize_observer_border_box = /* @__PURE__ */ new ResizeObserverSingleton({
|
|
75
|
+
box: 'border-box',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
var resize_observer_device_pixel_content_box = /* @__PURE__ */ new ResizeObserverSingleton({
|
|
79
|
+
box: 'device-pixel-content-box',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} value
|
|
84
|
+
*/
|
|
85
|
+
function to_number(value) {
|
|
86
|
+
return value === '' ? null : +value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {HTMLInputElement} input
|
|
91
|
+
*/
|
|
92
|
+
function is_numberlike_input(input) {
|
|
93
|
+
var type = input.type;
|
|
94
|
+
return type === 'number' || type === 'range';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @param {HTMLOptionElement} option */
|
|
98
|
+
function get_option_value(option) {
|
|
99
|
+
return option.value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Selects the correct option(s) (depending on whether this is a multiple select)
|
|
104
|
+
* @template V
|
|
105
|
+
* @param {HTMLSelectElement} select
|
|
106
|
+
* @param {V} value
|
|
107
|
+
* @param {boolean} mounting
|
|
108
|
+
*/
|
|
109
|
+
function select_option(select, value, mounting = false) {
|
|
110
|
+
if (select.multiple) {
|
|
111
|
+
// If value is null or undefined, keep the selection as is
|
|
112
|
+
if (value == undefined) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If not an array, warn and keep the selection as is
|
|
117
|
+
if (!is_array(value)) {
|
|
118
|
+
// TODO
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Otherwise, update the selection
|
|
122
|
+
for (var option of select.options) {
|
|
123
|
+
option.selected = /** @type {string[]} */ (value).includes(get_option_value(option));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (option of select.options) {
|
|
130
|
+
var option_value = get_option_value(option);
|
|
131
|
+
if (option_value === value) {
|
|
132
|
+
option.selected = true;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!mounting || value !== undefined) {
|
|
138
|
+
select.selectedIndex = -1; // no option should be selected
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {unknown} maybe_tracked
|
|
144
|
+
* @returns {(node: HTMLInputElement | HTMLSelectElement) => void}
|
|
145
|
+
*/
|
|
146
|
+
export function bindValue(maybe_tracked) {
|
|
147
|
+
if (!is_tracked_object(maybe_tracked)) {
|
|
148
|
+
throw new TypeError('bindValue() argument is not a tracked object');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var block = /** @type {Block} */ (active_block);
|
|
152
|
+
var tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
153
|
+
|
|
154
|
+
return (node) => {
|
|
155
|
+
var clear_event;
|
|
156
|
+
|
|
157
|
+
if (node.tagName === 'SELECT') {
|
|
158
|
+
var select = /** @type {HTMLSelectElement} */ (node);
|
|
159
|
+
var mounting = true;
|
|
160
|
+
|
|
161
|
+
clear_event = on(select, 'change', async () => {
|
|
162
|
+
var query = ':checked';
|
|
163
|
+
/** @type {unknown} */
|
|
164
|
+
var value;
|
|
165
|
+
|
|
166
|
+
if (select.multiple) {
|
|
167
|
+
value = [].map.call(select.querySelectorAll(query), get_option_value);
|
|
168
|
+
} else {
|
|
169
|
+
/** @type {HTMLOptionElement | null} */
|
|
170
|
+
var selected_option =
|
|
171
|
+
select.querySelector(query) ??
|
|
172
|
+
// will fall back to first non-disabled option if no option is selected
|
|
173
|
+
select.querySelector('option:not([disabled])');
|
|
174
|
+
value = selected_option && get_option_value(selected_option);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
set(tracked, value, block);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
effect(() => {
|
|
181
|
+
var value = get(tracked);
|
|
182
|
+
select_option(select, value, mounting);
|
|
183
|
+
|
|
184
|
+
// Mounting and value undefined -> take selection from dom
|
|
185
|
+
if (mounting && value === undefined) {
|
|
186
|
+
/** @type {HTMLOptionElement | null} */
|
|
187
|
+
var selected_option = select.querySelector(':checked');
|
|
188
|
+
if (selected_option !== null) {
|
|
189
|
+
value = get_option_value(selected_option);
|
|
190
|
+
set(tracked, value, block);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
mounting = false;
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
var input = /** @type {HTMLInputElement} */ (node);
|
|
198
|
+
|
|
199
|
+
clear_event = on(input, 'input', async () => {
|
|
200
|
+
/** @type {any} */
|
|
201
|
+
var value = input.value;
|
|
202
|
+
value = is_numberlike_input(input) ? to_number(value) : value;
|
|
203
|
+
set(tracked, value, block);
|
|
204
|
+
|
|
205
|
+
await tick();
|
|
206
|
+
|
|
207
|
+
if (value !== (value = get(tracked))) {
|
|
208
|
+
var start = input.selectionStart;
|
|
209
|
+
var end = input.selectionEnd;
|
|
210
|
+
input.value = value ?? '';
|
|
211
|
+
|
|
212
|
+
// Restore selection
|
|
213
|
+
if (end !== null) {
|
|
214
|
+
input.selectionStart = start;
|
|
215
|
+
input.selectionEnd = Math.min(end, input.value.length);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
render(() => {
|
|
221
|
+
var value = get(tracked);
|
|
222
|
+
|
|
223
|
+
if (is_numberlike_input(input) && value === to_number(input.value)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (input.type === 'date' && !value && !input.value) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (value !== input.value) {
|
|
232
|
+
input.value = value ?? '';
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return clear_event;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {unknown} maybe_tracked
|
|
243
|
+
* @returns {(node: HTMLInputElement) => void}
|
|
244
|
+
*/
|
|
245
|
+
export function bindChecked(maybe_tracked) {
|
|
246
|
+
if (!is_tracked_object(maybe_tracked)) {
|
|
247
|
+
throw new TypeError('bindChecked() argument is not a tracked object');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const block = /** @type {any} */ (active_block);
|
|
251
|
+
const tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
252
|
+
|
|
253
|
+
return (input) => {
|
|
254
|
+
const clear_event = on(input, 'change', () => {
|
|
255
|
+
set(tracked, input.checked, block);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return clear_event;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {unknown} maybe_tracked
|
|
264
|
+
* @param {'clientWidth' | 'clientHeight' | 'offsetWidth' | 'offsetHeight'} type
|
|
265
|
+
*/
|
|
266
|
+
function bind_element_size(maybe_tracked, type) {
|
|
267
|
+
if (!is_tracked_object(maybe_tracked)) {
|
|
268
|
+
throw new TypeError(
|
|
269
|
+
`bind${type.charAt(0).toUpperCase() + type.slice(1)}() argument is not a tracked object`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
var block = /** @type {any} */ (active_block);
|
|
274
|
+
var tracked = /** @type {Tracked<any>} */ (maybe_tracked);
|
|
275
|
+
|
|
276
|
+
return (/** @type {HTMLElement} */ element) => {
|
|
277
|
+
var unsubscribe = resize_observer_border_box.observe(element, () =>
|
|
278
|
+
set(tracked, element[type], block),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
effect(() => {
|
|
282
|
+
untrack(() => set(tracked, element[type], block));
|
|
283
|
+
return unsubscribe;
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {unknown} maybe_tracked
|
|
290
|
+
* @returns {(node: HTMLElement) => void}
|
|
291
|
+
*/
|
|
292
|
+
export function bindClientWidth(maybe_tracked) {
|
|
293
|
+
return bind_element_size(maybe_tracked, 'clientWidth');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {unknown} maybe_tracked
|
|
298
|
+
* @returns {(node: HTMLElement) => void}
|
|
299
|
+
*/
|
|
300
|
+
export function bindClientHeight(maybe_tracked) {
|
|
301
|
+
return bind_element_size(maybe_tracked, 'clientHeight');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @param {unknown} maybe_tracked
|
|
306
|
+
* @returns {(node: HTMLElement) => void}
|
|
307
|
+
*/
|
|
308
|
+
export function bindOffsetWidth(maybe_tracked) {
|
|
309
|
+
return bind_element_size(maybe_tracked, 'offsetWidth');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* @param {unknown} maybe_tracked
|
|
314
|
+
* @returns {(node: HTMLElement) => void}
|
|
315
|
+
*/
|
|
316
|
+
export function bindOffsetHeight(maybe_tracked) {
|
|
317
|
+
return bind_element_size(maybe_tracked, 'offsetHeight');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @param {unknown} maybe_tracked
|
|
322
|
+
* @param {'contentRect' | 'contentBoxSize' | 'borderBoxSize' | 'devicePixelContentBoxSize'} type
|
|
323
|
+
*/
|
|
324
|
+
function bind_element_rect(maybe_tracked, type) {
|
|
325
|
+
if (!is_tracked_object(maybe_tracked)) {
|
|
326
|
+
throw new TypeError(
|
|
327
|
+
`bind${type.charAt(0).toUpperCase() + type.slice(1)}() argument is not a tracked object`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
var block = /** @type {any} */ (active_block);
|
|
332
|
+
var tracked = /** @type {Tracked<any>} */ (maybe_tracked);
|
|
333
|
+
var observer =
|
|
334
|
+
type === 'contentRect' || type === 'contentBoxSize'
|
|
335
|
+
? resize_observer_content_box
|
|
336
|
+
: type === 'borderBoxSize'
|
|
337
|
+
? resize_observer_border_box
|
|
338
|
+
: resize_observer_device_pixel_content_box;
|
|
339
|
+
|
|
340
|
+
return (/** @type {HTMLElement} */ element) => {
|
|
341
|
+
var unsubscribe = observer.observe(
|
|
342
|
+
element,
|
|
343
|
+
/** @param {any} entry */ (entry) => set(tracked, entry[type], block),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
effect(() => unsubscribe);
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @param {unknown} maybe_tracked
|
|
352
|
+
* @returns {(node: HTMLElement) => void}
|
|
353
|
+
*/
|
|
354
|
+
export function bindContentRect(maybe_tracked) {
|
|
355
|
+
return bind_element_rect(maybe_tracked, 'contentRect');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @param {unknown} maybe_tracked
|
|
360
|
+
* @returns {(node: HTMLElement) => void}
|
|
361
|
+
*/
|
|
362
|
+
export function bindContentBoxSize(maybe_tracked) {
|
|
363
|
+
return bind_element_rect(maybe_tracked, 'contentBoxSize');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* @param {unknown} maybe_tracked
|
|
368
|
+
* @returns {(node: HTMLElement) => void}
|
|
369
|
+
*/
|
|
370
|
+
export function bindBorderBoxSize(maybe_tracked) {
|
|
371
|
+
return bind_element_rect(maybe_tracked, 'borderBoxSize');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @param {unknown} maybe_tracked
|
|
376
|
+
* @returns {(node: HTMLElement) => void}
|
|
377
|
+
*/
|
|
378
|
+
export function bindDevicePixelContentBoxSize(maybe_tracked) {
|
|
379
|
+
return bind_element_rect(maybe_tracked, 'devicePixelContentBoxSize');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* @param {unknown} maybe_tracked
|
|
384
|
+
* @param {'innerHTML' | 'innerText' | 'textContent'} property
|
|
385
|
+
* @returns {(node: HTMLElement) => void}
|
|
386
|
+
*/
|
|
387
|
+
export function bind_content_editable(maybe_tracked, property) {
|
|
388
|
+
if (!is_tracked_object(maybe_tracked)) {
|
|
389
|
+
throw new TypeError(
|
|
390
|
+
`bind${property.charAt(0).toUpperCase() + property.slice(1)}() argument is not a tracked object`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const block = /** @type {any} */ (active_block);
|
|
395
|
+
const tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
396
|
+
|
|
397
|
+
return (element) => {
|
|
398
|
+
const clear_event = on(element, 'input', () => {
|
|
399
|
+
set(tracked, element[property], block);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
render(() => {
|
|
403
|
+
var value = get(tracked);
|
|
404
|
+
|
|
405
|
+
if (element[property] !== value) {
|
|
406
|
+
if (value == null) {
|
|
407
|
+
// @ts-ignore
|
|
408
|
+
var non_null_value = element[property];
|
|
409
|
+
set(tracked, non_null_value, block);
|
|
410
|
+
} else {
|
|
411
|
+
// @ts-ignore
|
|
412
|
+
element[property] = value + '';
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return clear_event;
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* @param {unknown} maybe_tracked
|
|
423
|
+
* @returns {(node: HTMLElement) => void}
|
|
424
|
+
*/
|
|
425
|
+
export function bindInnerHTML(maybe_tracked) {
|
|
426
|
+
return bind_content_editable(maybe_tracked, 'innerHTML');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* @param {unknown} maybe_tracked
|
|
431
|
+
* @returns {(node: HTMLElement) => void}
|
|
432
|
+
*/
|
|
433
|
+
export function bindInnerText(maybe_tracked) {
|
|
434
|
+
return bind_content_editable(maybe_tracked, 'innerText');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* @param {unknown} maybe_tracked
|
|
439
|
+
* @returns {(node: HTMLElement) => void}
|
|
440
|
+
*/
|
|
441
|
+
export function bindTextContent(maybe_tracked) {
|
|
442
|
+
return bind_content_editable(maybe_tracked, 'textContent');
|
|
443
|
+
}
|
|
@@ -65,6 +65,10 @@ export { tracked_array } from '../../array.js';
|
|
|
65
65
|
|
|
66
66
|
export { tracked_object } from '../../object.js';
|
|
67
67
|
|
|
68
|
+
export { tracked_map } from '../../map.js';
|
|
69
|
+
|
|
70
|
+
export { tracked_set } from '../../set.js';
|
|
71
|
+
|
|
68
72
|
export { head } from './head.js';
|
|
69
73
|
|
|
70
74
|
export { script } from './script.js';
|
package/src/runtime/map.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @import { Block, Tracked } from '#client' */
|
|
2
|
-
import { get, increment, safe_scope, set, tracked } from './internal/client/runtime.js';
|
|
2
|
+
import { get, increment, safe_scope, set, tracked, with_scope } from './internal/client/runtime.js';
|
|
3
3
|
|
|
4
4
|
const introspect_methods = ['entries', 'forEach', 'values', Symbol.iterator];
|
|
5
5
|
|
|
@@ -193,3 +193,13 @@ export class TrackedMap extends Map {
|
|
|
193
193
|
return [...this];
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @template K, V
|
|
199
|
+
* @param {Block} block
|
|
200
|
+
* @param {...any} args
|
|
201
|
+
* @returns {TrackedMap<K, V>}
|
|
202
|
+
*/
|
|
203
|
+
export function tracked_map(block, ...args) {
|
|
204
|
+
return with_scope(block, () => new TrackedMap(...args));
|
|
205
|
+
}
|
package/src/runtime/set.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @import { Block, Tracked } from '#client' */
|
|
2
|
-
import { get, increment, safe_scope, set, tracked } from './internal/client/runtime.js';
|
|
2
|
+
import { get, increment, safe_scope, set, tracked, with_scope } from './internal/client/runtime.js';
|
|
3
3
|
|
|
4
4
|
const introspect_methods = ['entries', 'forEach', 'keys', 'values', Symbol.iterator];
|
|
5
5
|
|
|
@@ -193,3 +193,13 @@ export class TrackedSet extends Set {
|
|
|
193
193
|
return [...this];
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @template V
|
|
199
|
+
* @param {Block} block
|
|
200
|
+
* @param {...any} args
|
|
201
|
+
* @returns {TrackedSet<V>}
|
|
202
|
+
*/
|
|
203
|
+
export function tracked_set(block, ...args) {
|
|
204
|
+
return with_scope(block, () => new TrackedSet(...args));
|
|
205
|
+
}
|
|
@@ -138,4 +138,85 @@ describe('TrackedMap', () => {
|
|
|
138
138
|
|
|
139
139
|
expect(container.querySelectorAll('pre')[0].textContent).toBe('[["foo",1],["bar",2]]');
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
it('creates empty TrackedMap using #Map() shorthand syntax', () => {
|
|
143
|
+
component MapTest() {
|
|
144
|
+
let map = #Map();
|
|
145
|
+
|
|
146
|
+
<button onClick={() => map.set('a', 1)}>{'add'}</button>
|
|
147
|
+
<pre>{map.size}</pre>
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
render(MapTest);
|
|
151
|
+
|
|
152
|
+
expect(container.querySelector('pre').textContent).toBe('0');
|
|
153
|
+
|
|
154
|
+
const addButton = container.querySelector('button');
|
|
155
|
+
addButton.click();
|
|
156
|
+
flushSync();
|
|
157
|
+
|
|
158
|
+
expect(container.querySelector('pre').textContent).toBe('1');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
it('creates TrackedMap with initial entries using #Map() shorthand syntax', () => {
|
|
163
|
+
component MapTest() {
|
|
164
|
+
let map = #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
165
|
+
let value = track(() => map.get('b'));
|
|
166
|
+
|
|
167
|
+
<button onClick={() => map.set('b', 10)}>{'update'}</button>
|
|
168
|
+
<pre>{map.size}</pre>
|
|
169
|
+
<pre>{@value}</pre>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
render(MapTest);
|
|
173
|
+
|
|
174
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('3');
|
|
175
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
176
|
+
|
|
177
|
+
const updateButton = container.querySelector('button');
|
|
178
|
+
updateButton.click();
|
|
179
|
+
flushSync();
|
|
180
|
+
|
|
181
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('10');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('handles all operations with #Map() shorthand syntax', () => {
|
|
185
|
+
component MapTest() {
|
|
186
|
+
let map = #Map([['x', 100], ['y', 200]]);
|
|
187
|
+
let keys = track(() => Array.from(map.keys()));
|
|
188
|
+
|
|
189
|
+
<button onClick={() => map.set('z', 300)}>{'add'}</button>
|
|
190
|
+
<button onClick={() => map.delete('x')}>{'delete'}</button>
|
|
191
|
+
<button onClick={() => map.clear()}>{'clear'}</button>
|
|
192
|
+
|
|
193
|
+
<pre>{JSON.stringify(@keys)}</pre>
|
|
194
|
+
<pre>{map.size}</pre>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
render(MapTest);
|
|
198
|
+
|
|
199
|
+
const [addButton, deleteButton, clearButton] = container.querySelectorAll('button');
|
|
200
|
+
|
|
201
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('["x","y"]');
|
|
202
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
203
|
+
|
|
204
|
+
addButton.click();
|
|
205
|
+
flushSync();
|
|
206
|
+
|
|
207
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('["x","y","z"]');
|
|
208
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
|
|
209
|
+
|
|
210
|
+
deleteButton.click();
|
|
211
|
+
flushSync();
|
|
212
|
+
|
|
213
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('["y","z"]');
|
|
214
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
215
|
+
|
|
216
|
+
clearButton.click();
|
|
217
|
+
flushSync();
|
|
218
|
+
|
|
219
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[]');
|
|
220
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
|
|
221
|
+
});
|
|
141
222
|
});
|
|
@@ -96,4 +96,85 @@ describe('TrackedSet', () => {
|
|
|
96
96
|
|
|
97
97
|
expect(container.querySelectorAll('pre')[0].textContent).toBe('false');
|
|
98
98
|
});
|
|
99
|
+
|
|
100
|
+
it('creates empty TrackedSet using #Set() shorthand syntax', () => {
|
|
101
|
+
component SetTest() {
|
|
102
|
+
let items = #Set();
|
|
103
|
+
|
|
104
|
+
<button onClick={() => items.add(1)}>{'add'}</button>
|
|
105
|
+
<pre>{items.size}</pre>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
render(SetTest);
|
|
109
|
+
|
|
110
|
+
expect(container.querySelector('pre').textContent).toBe('0');
|
|
111
|
+
|
|
112
|
+
const addButton = container.querySelector('button');
|
|
113
|
+
addButton.click();
|
|
114
|
+
flushSync();
|
|
115
|
+
|
|
116
|
+
expect(container.querySelector('pre').textContent).toBe('1');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('creates TrackedSet with initial values using #Set() shorthand syntax', () => {
|
|
120
|
+
component SetTest() {
|
|
121
|
+
let items = #Set([1, 2, 3, 4]);
|
|
122
|
+
let hasValue = track(() => items.has(3));
|
|
123
|
+
|
|
124
|
+
<button onClick={() => items.delete(3)}>{'delete'}</button>
|
|
125
|
+
<pre>{items.size}</pre>
|
|
126
|
+
<pre>{@hasValue}</pre>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
render(SetTest);
|
|
130
|
+
|
|
131
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('4');
|
|
132
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('true');
|
|
133
|
+
|
|
134
|
+
const deleteButton = container.querySelector('button');
|
|
135
|
+
deleteButton.click();
|
|
136
|
+
flushSync();
|
|
137
|
+
|
|
138
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('3');
|
|
139
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('false');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('handles all operations with #Set() shorthand syntax', () => {
|
|
143
|
+
component SetTest() {
|
|
144
|
+
let items = #Set([10, 20, 30]);
|
|
145
|
+
let values = track(() => Array.from(items.values()));
|
|
146
|
+
|
|
147
|
+
<button onClick={() => items.add(40)}>{'add'}</button>
|
|
148
|
+
<button onClick={() => items.delete(20)}>{'delete'}</button>
|
|
149
|
+
<button onClick={() => items.clear()}>{'clear'}</button>
|
|
150
|
+
|
|
151
|
+
<pre>{JSON.stringify(@values)}</pre>
|
|
152
|
+
<pre>{items.size}</pre>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
render(SetTest);
|
|
156
|
+
|
|
157
|
+
const [addButton, deleteButton, clearButton] = container.querySelectorAll('button');
|
|
158
|
+
|
|
159
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[10,20,30]');
|
|
160
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
|
|
161
|
+
|
|
162
|
+
addButton.click();
|
|
163
|
+
flushSync();
|
|
164
|
+
|
|
165
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[10,20,30,40]');
|
|
166
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('4');
|
|
167
|
+
|
|
168
|
+
deleteButton.click();
|
|
169
|
+
flushSync();
|
|
170
|
+
|
|
171
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[10,30,40]');
|
|
172
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
|
|
173
|
+
|
|
174
|
+
clearButton.click();
|
|
175
|
+
flushSync();
|
|
176
|
+
|
|
177
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[]');
|
|
178
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
|
|
179
|
+
});
|
|
99
180
|
});
|