ripple 0.2.108 → 0.2.110
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 +31 -10
- package/src/compiler/phases/2-analyze/index.js +90 -1
- package/src/compiler/phases/3-transform/client/index.js +48 -7
- package/src/compiler/phases/3-transform/server/index.js +134 -8
- package/src/compiler/scope.js +15 -1
- package/src/compiler/types/index.d.ts +2 -2
- package/src/compiler/utils.js +2 -0
- package/src/runtime/internal/client/composite.js +37 -5
- package/src/runtime/internal/client/for.js +48 -20
- package/src/runtime/internal/client/index.js +2 -0
- package/src/runtime/internal/client/render.js +47 -20
- package/src/runtime/internal/client/rpc.js +14 -0
- package/src/utils/builders.js +15 -2
- package/tests/client/__snapshots__/compiler.test.ripple.snap +13 -0
- package/tests/client/__snapshots__/for.test.ripple.snap +5 -5
- package/tests/client/compiler.test.ripple +15 -0
- package/tests/client/composite.test.ripple +31 -0
- package/tests/client/dynamic-elements.test.ripple +207 -0
- package/tests/client/for.test.ripple +7 -7
- package/tests/client/switch.test.ripple +152 -0
- package/tests/server/__snapshots__/compiler.test.ripple.snap +37 -0
- package/tests/server/compiler.test.ripple +39 -0
- package/tests/server/switch.test.ripple +27 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @import { Block } from '#client' */
|
|
1
|
+
/** @import { Block, Tracked } from '#client' */
|
|
2
2
|
|
|
3
3
|
import { IS_CONTROLLED, IS_INDEXED } from '../../../constants.js';
|
|
4
4
|
import { branch, destroy_block, destroy_block_children, render } from './blocks.js';
|
|
@@ -12,31 +12,45 @@ import { array_from, is_array } from './utils.js';
|
|
|
12
12
|
* @param {Node} anchor
|
|
13
13
|
* @param {V} value
|
|
14
14
|
* @param {number} index
|
|
15
|
-
* @param {(anchor: Node, value: V, index?: any) => Block} render_fn
|
|
15
|
+
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
|
|
16
16
|
* @param {boolean} is_indexed
|
|
17
|
+
* @param {boolean} is_keyed
|
|
17
18
|
* @returns {Block}
|
|
18
19
|
*/
|
|
19
|
-
function create_item(anchor, value, index, render_fn, is_indexed) {
|
|
20
|
+
function create_item(anchor, value, index, render_fn, is_indexed, is_keyed) {
|
|
20
21
|
var b = branch(() => {
|
|
21
22
|
var tracked_index;
|
|
23
|
+
/** @type {V | Tracked} */
|
|
24
|
+
var tracked_value = value;
|
|
22
25
|
|
|
23
|
-
if (is_indexed) {
|
|
26
|
+
if (is_indexed || is_keyed) {
|
|
24
27
|
var block = /** @type {Block} */ (active_block);
|
|
25
28
|
|
|
26
29
|
if (block.s === null) {
|
|
27
|
-
|
|
30
|
+
if (is_indexed) {
|
|
31
|
+
tracked_index = tracked(index, block);
|
|
32
|
+
}
|
|
33
|
+
if (is_keyed) {
|
|
34
|
+
tracked_value = tracked(value, block);
|
|
35
|
+
}
|
|
28
36
|
|
|
29
37
|
block.s = {
|
|
30
38
|
start: null,
|
|
31
39
|
end: null,
|
|
32
40
|
i: tracked_index,
|
|
41
|
+
v: tracked_value,
|
|
33
42
|
};
|
|
34
43
|
} else {
|
|
35
|
-
|
|
44
|
+
if (is_indexed) {
|
|
45
|
+
tracked_index = block.s.i;
|
|
46
|
+
}
|
|
47
|
+
if (is_keyed) {
|
|
48
|
+
tracked_index = block.s.v;
|
|
49
|
+
}
|
|
36
50
|
}
|
|
37
|
-
render_fn(anchor,
|
|
51
|
+
render_fn(anchor, tracked_value, tracked_index);
|
|
38
52
|
} else {
|
|
39
|
-
render_fn(anchor,
|
|
53
|
+
render_fn(anchor, tracked_value);
|
|
40
54
|
}
|
|
41
55
|
});
|
|
42
56
|
return b;
|
|
@@ -87,7 +101,7 @@ function collection_to_array(collection) {
|
|
|
87
101
|
* @template V
|
|
88
102
|
* @param {Element} node
|
|
89
103
|
* @param {() => V[] | Iterable<V>} get_collection
|
|
90
|
-
* @param {(anchor: Node, value: V, index?: any) => Block} render_fn
|
|
104
|
+
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
|
|
91
105
|
* @param {number} flags
|
|
92
106
|
* @returns {void}
|
|
93
107
|
*/
|
|
@@ -116,7 +130,7 @@ export function for_block(node, get_collection, render_fn, flags) {
|
|
|
116
130
|
* @template K
|
|
117
131
|
* @param {Element} node
|
|
118
132
|
* @param {() => V[] | Iterable<V>} get_collection
|
|
119
|
-
* @param {(anchor: Node, value: V, index?: any) => Block} render_fn
|
|
133
|
+
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
|
|
120
134
|
* @param {number} flags
|
|
121
135
|
* @param {(item: V) => K} [get_key]
|
|
122
136
|
* @returns {void}
|
|
@@ -175,13 +189,22 @@ function update_index(block, index) {
|
|
|
175
189
|
set(block.s.i, index, block);
|
|
176
190
|
}
|
|
177
191
|
|
|
192
|
+
/**
|
|
193
|
+
* @param {Block} block
|
|
194
|
+
* @param {any} value
|
|
195
|
+
* @returns {void}
|
|
196
|
+
*/
|
|
197
|
+
function update_value(block, value) {
|
|
198
|
+
set(block.s.v, value, block);
|
|
199
|
+
}
|
|
200
|
+
|
|
178
201
|
/**
|
|
179
202
|
* @template V
|
|
180
203
|
* @template K
|
|
181
204
|
* @param {Element | Text} anchor
|
|
182
205
|
* @param {Block} block
|
|
183
206
|
* @param {V[]} b
|
|
184
|
-
* @param {(anchor: Node, value: V, index?: any) => Block} render_fn
|
|
207
|
+
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
|
|
185
208
|
* @param {boolean} is_controlled
|
|
186
209
|
* @param {boolean} is_indexed
|
|
187
210
|
* @param {(item: V) => K} get_key
|
|
@@ -236,7 +259,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
236
259
|
// Fast-path for create
|
|
237
260
|
if (a_length === 0) {
|
|
238
261
|
for (; j < b_length; j++) {
|
|
239
|
-
b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed);
|
|
262
|
+
b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, true);
|
|
240
263
|
}
|
|
241
264
|
state.array = b;
|
|
242
265
|
state.blocks = b_blocks;
|
|
@@ -261,6 +284,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
261
284
|
if (is_indexed) {
|
|
262
285
|
update_index(b_block, j);
|
|
263
286
|
}
|
|
287
|
+
update_value(b_block, b_val);
|
|
264
288
|
++j;
|
|
265
289
|
if (j > a_end || j > b_end) {
|
|
266
290
|
break outer;
|
|
@@ -282,6 +306,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
282
306
|
if (is_indexed) {
|
|
283
307
|
update_index(b_block, b_end);
|
|
284
308
|
}
|
|
309
|
+
update_value(b_block, b_val);
|
|
285
310
|
a_end--;
|
|
286
311
|
b_end--;
|
|
287
312
|
if (j > a_end || j > b_end) {
|
|
@@ -301,7 +326,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
301
326
|
while (j <= b_end) {
|
|
302
327
|
b_val = b[j];
|
|
303
328
|
var target = j >= a_length ? anchor : a_blocks[j].s.start;
|
|
304
|
-
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed);
|
|
329
|
+
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, true);
|
|
305
330
|
j++;
|
|
306
331
|
}
|
|
307
332
|
}
|
|
@@ -348,6 +373,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
348
373
|
if (is_indexed) {
|
|
349
374
|
update_index(b_block, j);
|
|
350
375
|
}
|
|
376
|
+
update_value(b_block, b_val);
|
|
351
377
|
++patched;
|
|
352
378
|
break;
|
|
353
379
|
}
|
|
@@ -387,9 +413,11 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
387
413
|
pos = j;
|
|
388
414
|
}
|
|
389
415
|
block = b_blocks[j] = a_blocks[i];
|
|
416
|
+
b_val = b[j];
|
|
390
417
|
if (is_indexed) {
|
|
391
418
|
update_index(block, j);
|
|
392
419
|
}
|
|
420
|
+
update_value(b_block, b_val);
|
|
393
421
|
++patched;
|
|
394
422
|
} else if (!fast_path_removal) {
|
|
395
423
|
destroy_block(a_blocks[i]);
|
|
@@ -417,7 +445,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
417
445
|
next_pos = pos + 1;
|
|
418
446
|
|
|
419
447
|
var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
|
|
420
|
-
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
|
|
448
|
+
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
|
|
421
449
|
} else if (j < 0 || i !== seq[j]) {
|
|
422
450
|
pos = i + b_start;
|
|
423
451
|
b_val = b[pos];
|
|
@@ -437,7 +465,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
437
465
|
next_pos = pos + 1;
|
|
438
466
|
|
|
439
467
|
var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
|
|
440
|
-
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
|
|
468
|
+
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
|
|
441
469
|
}
|
|
442
470
|
}
|
|
443
471
|
}
|
|
@@ -452,7 +480,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
452
480
|
* @param {Element | Text} anchor
|
|
453
481
|
* @param {Block} block
|
|
454
482
|
* @param {V[]} b
|
|
455
|
-
* @param {(anchor: Node, value: V, index?: any) => Block} render_fn
|
|
483
|
+
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
|
|
456
484
|
* @param {boolean} is_controlled
|
|
457
485
|
* @param {boolean} is_indexed
|
|
458
486
|
* @returns {void}
|
|
@@ -505,7 +533,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
505
533
|
// Fast-path for create
|
|
506
534
|
if (a_length === 0) {
|
|
507
535
|
for (; j < b_length; j++) {
|
|
508
|
-
b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed);
|
|
536
|
+
b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, false);
|
|
509
537
|
}
|
|
510
538
|
state.array = b;
|
|
511
539
|
state.blocks = b_blocks;
|
|
@@ -560,7 +588,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
560
588
|
while (j <= b_end) {
|
|
561
589
|
b_val = b[j];
|
|
562
590
|
var target = j >= a_length ? anchor : a_blocks[j].s.start;
|
|
563
|
-
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed);
|
|
591
|
+
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, false);
|
|
564
592
|
j++;
|
|
565
593
|
}
|
|
566
594
|
}
|
|
@@ -673,7 +701,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
673
701
|
next_pos = pos + 1;
|
|
674
702
|
|
|
675
703
|
var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
|
|
676
|
-
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
|
|
704
|
+
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
|
|
677
705
|
} else if (j < 0 || i !== seq[j]) {
|
|
678
706
|
pos = i + b_start;
|
|
679
707
|
b_val = b[pos];
|
|
@@ -693,7 +721,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
693
721
|
next_pos = pos + 1;
|
|
694
722
|
|
|
695
723
|
var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
|
|
696
|
-
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
|
|
724
|
+
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
|
|
697
725
|
}
|
|
698
726
|
}
|
|
699
727
|
}
|
|
@@ -128,39 +128,66 @@ export function apply_styles(element, newStyles) {
|
|
|
128
128
|
* @returns {void}
|
|
129
129
|
*/
|
|
130
130
|
export function set_attributes(element, attributes) {
|
|
131
|
+
let found_enumerable_keys = false;
|
|
132
|
+
|
|
131
133
|
for (const key in attributes) {
|
|
132
134
|
if (key === 'children') continue;
|
|
135
|
+
found_enumerable_keys = true;
|
|
133
136
|
|
|
134
137
|
let value = attributes[key];
|
|
135
|
-
|
|
136
138
|
if (is_tracked_object(value)) {
|
|
137
139
|
value = get(value);
|
|
138
140
|
}
|
|
141
|
+
set_attribute_helper(element, key, value);
|
|
142
|
+
}
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Use delegation for delegated events
|
|
152
|
-
/** @type {any} */ (element)['__' + event_name] = value;
|
|
153
|
-
delegate([event_name]);
|
|
154
|
-
} else {
|
|
155
|
-
// Use addEventListener for non-delegated events
|
|
156
|
-
event(event_name, element, value);
|
|
144
|
+
// Only if no enumerable keys but attributes object exists
|
|
145
|
+
// This handles spread_props Proxy objects from dynamic elements with {...spread}
|
|
146
|
+
if (!found_enumerable_keys && attributes) {
|
|
147
|
+
const allKeys = Reflect.ownKeys(attributes);
|
|
148
|
+
for (const key of allKeys) {
|
|
149
|
+
if (key === 'children') continue;
|
|
150
|
+
if (typeof key === 'symbol') continue; // Skip symbols - handled by apply_element_spread
|
|
151
|
+
|
|
152
|
+
let value = attributes[key];
|
|
153
|
+
if (is_tracked_object(value)) {
|
|
154
|
+
value = get(value);
|
|
157
155
|
}
|
|
158
|
-
|
|
159
|
-
set_attribute(element, key, value);
|
|
156
|
+
set_attribute_helper(element, key, value);
|
|
160
157
|
}
|
|
161
158
|
}
|
|
162
159
|
}
|
|
163
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Helper function to set a single attribute
|
|
163
|
+
* @param {Element} element
|
|
164
|
+
* @param {string} key
|
|
165
|
+
* @param {any} value
|
|
166
|
+
*/
|
|
167
|
+
function set_attribute_helper(element, key, value) {
|
|
168
|
+
if (key === 'class') {
|
|
169
|
+
const is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
|
|
170
|
+
set_class(/** @type {HTMLElement} */ (element), value, undefined, is_html);
|
|
171
|
+
} else if (key === '#class') {
|
|
172
|
+
// Special case for static class when spreading props
|
|
173
|
+
element.classList.add(value);
|
|
174
|
+
} else if (typeof key === 'string' && is_event_attribute(key)) {
|
|
175
|
+
// Handle event handlers in spread props
|
|
176
|
+
const event_name = get_attribute_event_name(key);
|
|
177
|
+
|
|
178
|
+
if (is_delegated(event_name)) {
|
|
179
|
+
// Use delegation for delegated events
|
|
180
|
+
/** @type {any} */ (element)['__' + event_name] = value;
|
|
181
|
+
delegate([event_name]);
|
|
182
|
+
} else {
|
|
183
|
+
// Use addEventListener for non-delegated events
|
|
184
|
+
event(event_name, element, value);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
set_attribute(element, key, value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
164
191
|
/**
|
|
165
192
|
* @param {import('clsx').ClassValue} value
|
|
166
193
|
* @param {string} [hash]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} hash
|
|
4
|
+
* @param {any[]} args
|
|
5
|
+
*/
|
|
6
|
+
export function rpc(hash, args) {
|
|
7
|
+
return fetch('/_$_ripple_rpc_$_/' + hash, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: {
|
|
10
|
+
'Content-Type': 'application/json'
|
|
11
|
+
},
|
|
12
|
+
body: JSON.stringify(args)
|
|
13
|
+
}).then(res => res.json());
|
|
14
|
+
}
|
package/src/utils/builders.js
CHANGED
|
@@ -220,6 +220,17 @@ export function export_default(declaration) {
|
|
|
220
220
|
return { type: 'ExportDefaultDeclaration', declaration };
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* @param {ESTree.Declaration | null} declaration
|
|
225
|
+
* @param {ESTree.ExportSpecifier[]} [specifiers]
|
|
226
|
+
* @param {ESTree.ImportAttribute[]} [attributes]
|
|
227
|
+
* @param {ESTree.Literal | null} [source]
|
|
228
|
+
* @returns {ESTree.ExportNamedDeclaration}
|
|
229
|
+
*/
|
|
230
|
+
export function export_builder(declaration, specifiers = [], attributes = [], source = null) {
|
|
231
|
+
return { type: 'ExportNamedDeclaration', declaration, specifiers, attributes, source };
|
|
232
|
+
}
|
|
233
|
+
|
|
223
234
|
/**
|
|
224
235
|
* @param {ESTree.Identifier} id
|
|
225
236
|
* @param {ESTree.Pattern[]} params
|
|
@@ -581,16 +592,17 @@ export function method(kind, key, params, body, computed = false, is_static = fa
|
|
|
581
592
|
* @param {ESTree.Identifier | null} id
|
|
582
593
|
* @param {ESTree.Pattern[]} params
|
|
583
594
|
* @param {ESTree.BlockStatement} body
|
|
595
|
+
* @param {boolean} async
|
|
584
596
|
* @returns {ESTree.FunctionExpression}
|
|
585
597
|
*/
|
|
586
|
-
function function_builder(id, params, body) {
|
|
598
|
+
function function_builder(id, params, body, async = false) {
|
|
587
599
|
return {
|
|
588
600
|
type: 'FunctionExpression',
|
|
589
601
|
id,
|
|
590
602
|
params,
|
|
591
603
|
body,
|
|
592
604
|
generator: false,
|
|
593
|
-
async
|
|
605
|
+
async,
|
|
594
606
|
metadata: /** @type {any} */ (null), // should not be used by codegen
|
|
595
607
|
};
|
|
596
608
|
}
|
|
@@ -812,6 +824,7 @@ export {
|
|
|
812
824
|
let_builder as let,
|
|
813
825
|
const_builder as const,
|
|
814
826
|
var_builder as var,
|
|
827
|
+
export_builder as export,
|
|
815
828
|
true_instance as true,
|
|
816
829
|
false_instance as false,
|
|
817
830
|
break_statement as break,
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
+
exports[`compiler success tests > compiles TSInstantiationExpression 1`] = `
|
|
4
|
+
"import * as _$_ from 'ripple/internal/client';
|
|
5
|
+
|
|
6
|
+
function makeBox(value) {
|
|
7
|
+
return { value };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const makeStringBox = (makeBox);
|
|
11
|
+
const stringBox = makeStringBox('abc');
|
|
12
|
+
const ErrorMap = (Map);
|
|
13
|
+
const errorMap = new ErrorMap();"
|
|
14
|
+
`;
|
|
15
|
+
|
|
3
16
|
exports[`compiler success tests > compiles tracked values in effect with assignment expression 1`] = `"state.count = _$_.get(count);"`;
|
|
4
17
|
|
|
5
18
|
exports[`compiler success tests > compiles tracked values in effect with update expressions 1`] = `
|
|
@@ -198,7 +198,7 @@ exports[`for statements > correctly handles the index in a for...of loop 3`] = `
|
|
|
198
198
|
</div>
|
|
199
199
|
`;
|
|
200
200
|
|
|
201
|
-
exports[`for statements > handles
|
|
201
|
+
exports[`for statements > handles updating with new objects with same key 1`] = `
|
|
202
202
|
<div>
|
|
203
203
|
<!---->
|
|
204
204
|
<div>
|
|
@@ -218,17 +218,17 @@ exports[`for statements > handles reversing an array manually 1`] = `
|
|
|
218
218
|
</div>
|
|
219
219
|
`;
|
|
220
220
|
|
|
221
|
-
exports[`for statements > handles
|
|
221
|
+
exports[`for statements > handles updating with new objects with same key 2`] = `
|
|
222
222
|
<div>
|
|
223
223
|
<!---->
|
|
224
224
|
<div>
|
|
225
|
-
0:Item
|
|
225
|
+
0:Item 1!
|
|
226
226
|
</div>
|
|
227
227
|
<div>
|
|
228
|
-
1:Item 2
|
|
228
|
+
1:Item 2!
|
|
229
229
|
</div>
|
|
230
230
|
<div>
|
|
231
|
-
2:Item
|
|
231
|
+
2:Item 3!
|
|
232
232
|
</div>
|
|
233
233
|
<!---->
|
|
234
234
|
<button>
|
|
@@ -495,4 +495,19 @@ effect(() => {
|
|
|
495
495
|
const effectMatch = result.js.code.match(/effect\(\(\) => \{([\s\S]+?)\n\t\}\)\)/);
|
|
496
496
|
expect(effectMatch[1].trim()).toMatchSnapshot();
|
|
497
497
|
});
|
|
498
|
+
|
|
499
|
+
it('compiles TSInstantiationExpression', () => {
|
|
500
|
+
const source =
|
|
501
|
+
`function makeBox<T>(value: T) {
|
|
502
|
+
return { value };
|
|
503
|
+
}
|
|
504
|
+
const makeStringBox = makeBox<string>;
|
|
505
|
+
const stringBox = makeStringBox('abc');
|
|
506
|
+
const ErrorMap = Map<string, Error>;
|
|
507
|
+
const errorMap = new ErrorMap();`;
|
|
508
|
+
|
|
509
|
+
const result = compile(source, 'test.ripple', { mode: 'client' });
|
|
510
|
+
|
|
511
|
+
expect(result.js.code).toMatchSnapshot();
|
|
512
|
+
});
|
|
498
513
|
});
|
|
@@ -659,4 +659,35 @@ describe('composite components', () => {
|
|
|
659
659
|
|
|
660
660
|
expect(container.querySelector('#container').textContent).toBe('I am child 1');
|
|
661
661
|
});
|
|
662
|
+
|
|
663
|
+
it('mutating a tracked value prop should work as intended', () => {
|
|
664
|
+
const logs = [];
|
|
665
|
+
|
|
666
|
+
component Counter({count}) {
|
|
667
|
+
effect(() => {
|
|
668
|
+
logs.push(@count);
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
<button onClick={() => @count = @count + 1}>{'+'}</button>
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
component App() {
|
|
675
|
+
const count = track(0);
|
|
676
|
+
|
|
677
|
+
<div>
|
|
678
|
+
<Counter count={count} />
|
|
679
|
+
</div>
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
render(App);
|
|
683
|
+
flushSync();
|
|
684
|
+
|
|
685
|
+
expect(logs).toEqual([0]);
|
|
686
|
+
|
|
687
|
+
const button = container.querySelector('button');
|
|
688
|
+
button.click();
|
|
689
|
+
flushSync();
|
|
690
|
+
|
|
691
|
+
expect(logs).toEqual([0, 1]);
|
|
692
|
+
})
|
|
662
693
|
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mount, flushSync, track, createRefKey } from 'ripple';
|
|
3
|
+
|
|
4
|
+
describe('dynamic DOM elements', () => {
|
|
5
|
+
let container;
|
|
6
|
+
|
|
7
|
+
function render(component) {
|
|
8
|
+
mount(component, {
|
|
9
|
+
target: container,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
container = document.createElement('div');
|
|
15
|
+
document.body.appendChild(container);
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
document.body.removeChild(container);
|
|
19
|
+
container = null;
|
|
20
|
+
});
|
|
21
|
+
it('renders static dynamic element', () => {
|
|
22
|
+
component App() {
|
|
23
|
+
let tag = track('div');
|
|
24
|
+
|
|
25
|
+
<@tag>{'Hello World'}</@tag>
|
|
26
|
+
}
|
|
27
|
+
render(App);
|
|
28
|
+
|
|
29
|
+
const element = container.querySelector('div');
|
|
30
|
+
expect(element).toBeTruthy();
|
|
31
|
+
expect(element.textContent).toBe('Hello World');
|
|
32
|
+
});
|
|
33
|
+
it('renders reactive dynamic element', () => {
|
|
34
|
+
component App() {
|
|
35
|
+
let tag = track('div');
|
|
36
|
+
|
|
37
|
+
<button onClick={() => {
|
|
38
|
+
@tag = 'span';
|
|
39
|
+
}}>{'Change Tag'}</button>
|
|
40
|
+
<@tag id="dynamic">{'Hello World'}</@tag>
|
|
41
|
+
}
|
|
42
|
+
render(App);
|
|
43
|
+
// Initially should be a div
|
|
44
|
+
let dynamicElement = container.querySelector('#dynamic');
|
|
45
|
+
expect(dynamicElement.tagName).toBe('DIV');
|
|
46
|
+
expect(dynamicElement.textContent).toBe('Hello World');
|
|
47
|
+
// Click button to change tag
|
|
48
|
+
const button = container.querySelector('button');
|
|
49
|
+
button.click();
|
|
50
|
+
flushSync();
|
|
51
|
+
// Should now be a span
|
|
52
|
+
dynamicElement = container.querySelector('#dynamic');
|
|
53
|
+
expect(dynamicElement.tagName).toBe('SPAN');
|
|
54
|
+
expect(dynamicElement.textContent).toBe('Hello World');
|
|
55
|
+
});
|
|
56
|
+
it('renders self-closing dynamic element', () => {
|
|
57
|
+
component App() {
|
|
58
|
+
let tag = track('input');
|
|
59
|
+
|
|
60
|
+
<@tag type="text" value="test" />
|
|
61
|
+
}
|
|
62
|
+
render(App);
|
|
63
|
+
|
|
64
|
+
const element = container.querySelector('input');
|
|
65
|
+
expect(element).toBeTruthy();
|
|
66
|
+
expect(element.type).toBe('text');
|
|
67
|
+
expect(element.value).toBe('test');
|
|
68
|
+
});
|
|
69
|
+
it('handles dynamic element with attributes', () => {
|
|
70
|
+
component App() {
|
|
71
|
+
let tag = track('div');
|
|
72
|
+
let className = track('test-class');
|
|
73
|
+
|
|
74
|
+
<@tag class={@className} id="test" data-testid="dynamic-element">{'Content'}</@tag>
|
|
75
|
+
}
|
|
76
|
+
render(App);
|
|
77
|
+
|
|
78
|
+
const element = container.querySelector('#test');
|
|
79
|
+
expect(element.tagName).toBe('DIV');
|
|
80
|
+
expect(element.className).toBe('test-class');
|
|
81
|
+
expect(element.getAttribute('data-testid')).toBe('dynamic-element');
|
|
82
|
+
expect(element.textContent).toBe('Content');
|
|
83
|
+
});
|
|
84
|
+
it('handles nested dynamic elements', () => {
|
|
85
|
+
component App() {
|
|
86
|
+
let outerTag = track('div');
|
|
87
|
+
let innerTag = track('span');
|
|
88
|
+
|
|
89
|
+
<@outerTag class="outer">
|
|
90
|
+
<@innerTag class="inner">{'Nested content'}</@innerTag>
|
|
91
|
+
</@outerTag>
|
|
92
|
+
}
|
|
93
|
+
render(App);
|
|
94
|
+
|
|
95
|
+
const outer = container.querySelector('.outer');
|
|
96
|
+
const inner = container.querySelector('.inner');
|
|
97
|
+
|
|
98
|
+
expect(outer.tagName).toBe('DIV');
|
|
99
|
+
expect(inner.tagName).toBe('SPAN');
|
|
100
|
+
expect(inner.textContent).toBe('Nested content');
|
|
101
|
+
expect(outer.contains(inner)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
it('handles dynamic element with class object', () => {
|
|
104
|
+
component App() {
|
|
105
|
+
let tag = track('div');
|
|
106
|
+
let active = track(true);
|
|
107
|
+
|
|
108
|
+
<@tag class={{ active: @active, 'dynamic-element': true }}>
|
|
109
|
+
{'Element with class object'}
|
|
110
|
+
</@tag>
|
|
111
|
+
}
|
|
112
|
+
render(App);
|
|
113
|
+
|
|
114
|
+
const element = container.querySelector('div');
|
|
115
|
+
expect(element).toBeTruthy();
|
|
116
|
+
expect(element.classList.contains('active')).toBe(true);
|
|
117
|
+
expect(element.classList.contains('dynamic-element')).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it('handles dynamic element with style object', () => {
|
|
120
|
+
component App() {
|
|
121
|
+
let tag = track('span');
|
|
122
|
+
|
|
123
|
+
<@tag style={{
|
|
124
|
+
color: 'red',
|
|
125
|
+
fontSize: '16px',
|
|
126
|
+
fontWeight: 'bold'
|
|
127
|
+
}}>
|
|
128
|
+
{'Styled dynamic element'}
|
|
129
|
+
</@tag>
|
|
130
|
+
}
|
|
131
|
+
render(App);
|
|
132
|
+
|
|
133
|
+
const element = container.querySelector('span');
|
|
134
|
+
expect(element).toBeTruthy();
|
|
135
|
+
expect(element.style.color).toBe('red');
|
|
136
|
+
expect(element.style.fontSize).toBe('16px');
|
|
137
|
+
expect(element.style.fontWeight).toBe('bold');
|
|
138
|
+
});
|
|
139
|
+
it('handles dynamic element with spread attributes', () => {
|
|
140
|
+
component App() {
|
|
141
|
+
let tag = track('section');
|
|
142
|
+
const attrs = {
|
|
143
|
+
id: 'spread-section',
|
|
144
|
+
'data-testid': 'spread-test',
|
|
145
|
+
class: 'spread-class',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
<@tag {...attrs} data-extra="additional">
|
|
149
|
+
{'Element with spread attributes'}
|
|
150
|
+
</@tag>
|
|
151
|
+
}
|
|
152
|
+
render(App);
|
|
153
|
+
|
|
154
|
+
const element = container.querySelector('section');
|
|
155
|
+
expect(element).toBeTruthy();
|
|
156
|
+
expect(element.id).toBe('spread-section');
|
|
157
|
+
expect(element.getAttribute('data-testid')).toBe('spread-test');
|
|
158
|
+
expect(element.className).toBe('spread-class');
|
|
159
|
+
expect(element.getAttribute('data-extra')).toBe('additional');
|
|
160
|
+
});
|
|
161
|
+
it('handles dynamic element with ref', () => {
|
|
162
|
+
let capturedElement = null;
|
|
163
|
+
|
|
164
|
+
component App() {
|
|
165
|
+
let tag = track('article');
|
|
166
|
+
|
|
167
|
+
<@tag {ref (node) => { capturedElement = node; }} id="ref-test">
|
|
168
|
+
{'Element with ref'}
|
|
169
|
+
</@tag>
|
|
170
|
+
}
|
|
171
|
+
render(App);
|
|
172
|
+
flushSync();
|
|
173
|
+
expect(capturedElement).toBeTruthy();
|
|
174
|
+
expect(capturedElement.tagName).toBe('ARTICLE');
|
|
175
|
+
expect(capturedElement.id).toBe('ref-test');
|
|
176
|
+
expect(capturedElement.textContent).toBe('Element with ref');
|
|
177
|
+
});
|
|
178
|
+
it('handles dynamic element with createRefKey in spread', () => {
|
|
179
|
+
component App() {
|
|
180
|
+
let tag = track('header');
|
|
181
|
+
|
|
182
|
+
function elementRef(node) {
|
|
183
|
+
// Set an attribute on the element to prove ref was called
|
|
184
|
+
node.setAttribute('data-spread-ref-called', 'true');
|
|
185
|
+
node.setAttribute('data-spread-ref-tag', node.tagName.toLowerCase());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const dynamicProps = {
|
|
189
|
+
id: 'spread-ref-test',
|
|
190
|
+
class: 'ref-element',
|
|
191
|
+
[createRefKey()]: elementRef
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
<@tag {...dynamicProps}>{'Element with spread ref'}</@tag>
|
|
195
|
+
}
|
|
196
|
+
render(App);
|
|
197
|
+
flushSync();
|
|
198
|
+
|
|
199
|
+
// Check that the spread ref was called by verifying attributes were set
|
|
200
|
+
const element = container.querySelector('header');
|
|
201
|
+
expect(element).toBeTruthy();
|
|
202
|
+
expect(element.getAttribute('data-spread-ref-called')).toBe('true');
|
|
203
|
+
expect(element.getAttribute('data-spread-ref-tag')).toBe('header');
|
|
204
|
+
expect(element.id).toBe('spread-ref-test');
|
|
205
|
+
expect(element.className).toBe('ref-element');
|
|
206
|
+
});
|
|
207
|
+
});
|