ripple 0.2.43 → 0.2.45
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 +80 -5
- package/src/compiler/phases/2-analyze/index.js +23 -1
- package/src/compiler/phases/2-analyze/prune.js +3 -1
- package/src/compiler/phases/3-transform/index.js +49 -23
- package/src/compiler/phases/3-transform/segments.js +2 -2
- package/src/compiler/utils.js +15 -13
- package/src/runtime/array.js +118 -25
- package/src/runtime/index.js +2 -1
- package/src/runtime/internal/client/blocks.js +5 -5
- package/src/runtime/internal/client/constants.js +6 -3
- package/src/runtime/internal/client/index.js +2 -2
- package/src/runtime/internal/client/render.js +6 -6
- package/src/runtime/internal/client/runtime.js +4 -3
- package/tests/array.test.ripple +125 -37
- package/tests/basic.test.ripple +32 -5
- package/tests/{decorators.test.ripple → ref.test.ripple} +4 -4
- package/types/index.d.ts +2 -0
package/package.json
CHANGED
|
@@ -38,6 +38,82 @@ function RipplePlugin(config) {
|
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Override getTokenFromCode to handle @ as an identifier prefix
|
|
42
|
+
getTokenFromCode(code) {
|
|
43
|
+
if (code === 64) { // '@' character
|
|
44
|
+
// Look ahead to see if this is followed by a valid identifier character
|
|
45
|
+
if (this.pos + 1 < this.input.length) {
|
|
46
|
+
const nextChar = this.input.charCodeAt(this.pos + 1);
|
|
47
|
+
// Check if the next character can start an identifier
|
|
48
|
+
if ((nextChar >= 65 && nextChar <= 90) || // A-Z
|
|
49
|
+
(nextChar >= 97 && nextChar <= 122) || // a-z
|
|
50
|
+
nextChar === 95 || nextChar === 36) { // _ or $
|
|
51
|
+
|
|
52
|
+
// Check if we're in an expression context
|
|
53
|
+
// In JSX expressions, inside parentheses, assignments, etc.
|
|
54
|
+
// we want to treat @ as an identifier prefix rather than decorator
|
|
55
|
+
const currentType = this.type;
|
|
56
|
+
const inExpression = this.exprAllowed ||
|
|
57
|
+
currentType === tt.braceL || // Inside { }
|
|
58
|
+
currentType === tt.parenL || // Inside ( )
|
|
59
|
+
currentType === tt.eq || // After =
|
|
60
|
+
currentType === tt.comma || // After ,
|
|
61
|
+
currentType === tt.colon || // After :
|
|
62
|
+
currentType === tt.question || // After ?
|
|
63
|
+
currentType === tt.logicalOR || // After ||
|
|
64
|
+
currentType === tt.logicalAND; // After &&
|
|
65
|
+
|
|
66
|
+
if (inExpression) {
|
|
67
|
+
return this.readAtIdentifier();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return super.getTokenFromCode(code);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Read an @ prefixed identifier
|
|
76
|
+
readAtIdentifier() {
|
|
77
|
+
const start = this.pos;
|
|
78
|
+
this.pos++; // skip '@'
|
|
79
|
+
|
|
80
|
+
// Read the identifier part manually
|
|
81
|
+
let word = '';
|
|
82
|
+
while (this.pos < this.input.length) {
|
|
83
|
+
const ch = this.input.charCodeAt(this.pos);
|
|
84
|
+
if ((ch >= 65 && ch <= 90) || // A-Z
|
|
85
|
+
(ch >= 97 && ch <= 122) || // a-z
|
|
86
|
+
(ch >= 48 && ch <= 57) || // 0-9
|
|
87
|
+
ch === 95 || ch === 36) { // _ or $
|
|
88
|
+
word += this.input[this.pos++];
|
|
89
|
+
} else {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (word === '') {
|
|
95
|
+
this.raise(start, 'Invalid @ identifier');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Return the full identifier including @
|
|
99
|
+
return this.finishToken(tt.name, '@' + word);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Override parseIdent to mark @ identifiers as tracked
|
|
103
|
+
parseIdent(liberal) {
|
|
104
|
+
const node = super.parseIdent(liberal);
|
|
105
|
+
if (node.name && node.name.startsWith('@')) {
|
|
106
|
+
node.name = node.name.slice(1); // Remove the '@' for internal use
|
|
107
|
+
node.tracked = true;
|
|
108
|
+
node.start++;
|
|
109
|
+
const prev_pos = this.pos;
|
|
110
|
+
this.pos = node.start;
|
|
111
|
+
node.loc.start = this.curPosition();
|
|
112
|
+
this.pos = prev_pos;
|
|
113
|
+
}
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
|
|
41
117
|
parseExportDefaultDeclaration() {
|
|
42
118
|
// Check if this is "export default component"
|
|
43
119
|
if (this.value === 'component') {
|
|
@@ -139,15 +215,14 @@ function RipplePlugin(config) {
|
|
|
139
215
|
}
|
|
140
216
|
|
|
141
217
|
if (this.eat(tt.braceL)) {
|
|
142
|
-
if (this.
|
|
218
|
+
if (this.value === 'ref') {
|
|
143
219
|
this.next();
|
|
144
|
-
if (this.
|
|
145
|
-
this.
|
|
220
|
+
if (this.type === tt.braceR) {
|
|
221
|
+
this.raise(this.start, '"ref" is a Ripple keyword and must be used in the form {ref fn}');
|
|
146
222
|
}
|
|
147
|
-
this.next();
|
|
148
223
|
node.argument = this.parseMaybeAssign();
|
|
149
224
|
this.expect(tt.braceR);
|
|
150
|
-
return this.finishNode(node, '
|
|
225
|
+
return this.finishNode(node, 'RefAttribute');
|
|
151
226
|
} else if (this.type === tt.ellipsis) {
|
|
152
227
|
this.expect(tt.ellipsis);
|
|
153
228
|
node.argument = this.parseMaybeAssign();
|
|
@@ -118,12 +118,34 @@ const visitors = {
|
|
|
118
118
|
if (
|
|
119
119
|
is_reference(node, /** @type {Node} */ (parent)) &&
|
|
120
120
|
context.state.metadata?.tracking === false &&
|
|
121
|
-
is_tracked_name(node
|
|
121
|
+
is_tracked_name(node) &&
|
|
122
122
|
binding?.node !== node
|
|
123
123
|
) {
|
|
124
124
|
context.state.metadata.tracking = true;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
if (
|
|
128
|
+
is_reference(node, /** @type {Node} */ (parent)) &&
|
|
129
|
+
node.tracked &&
|
|
130
|
+
binding?.node !== node
|
|
131
|
+
) {
|
|
132
|
+
if (context.state.metadata?.tracking === false) {
|
|
133
|
+
context.state.metadata.tracking = true;
|
|
134
|
+
}
|
|
135
|
+
binding.transform = {
|
|
136
|
+
read_tracked: (node) => b.call('$.get_tracked', node),
|
|
137
|
+
assign_tracked: (node, value) => b.call('$.set', node, value, b.id('__block')),
|
|
138
|
+
update_tracked: (node) => {
|
|
139
|
+
return b.call(
|
|
140
|
+
node.prefix ? '$.update_pre' : '$.update',
|
|
141
|
+
node.argument,
|
|
142
|
+
b.id('__block'),
|
|
143
|
+
node.operator === '--' && b.literal(-1),
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
127
149
|
context.next();
|
|
128
150
|
},
|
|
129
151
|
|
|
@@ -405,7 +405,9 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
|
|
|
405
405
|
if (attribute.type === 'SpreadAttribute') return true;
|
|
406
406
|
|
|
407
407
|
if (attribute.type !== 'Attribute') continue;
|
|
408
|
-
|
|
408
|
+
|
|
409
|
+
const lowerCaseName = name.toLowerCase();
|
|
410
|
+
if (![lowerCaseName, `$${lowerCaseName}`].includes(attribute.name.name.toLowerCase())) continue;
|
|
409
411
|
|
|
410
412
|
if (expected_value === null) return true;
|
|
411
413
|
|
|
@@ -72,10 +72,15 @@ function build_getter(node, context) {
|
|
|
72
72
|
|
|
73
73
|
for (let i = context.path.length - 1; i >= 0; i -= 1) {
|
|
74
74
|
const binding = state.scope.get(node.name);
|
|
75
|
+
const transform = binding?.transform;
|
|
75
76
|
|
|
76
77
|
// don't transform the declaration itself
|
|
77
|
-
if (node !== binding?.node
|
|
78
|
-
|
|
78
|
+
if (node !== binding?.node) {
|
|
79
|
+
const read_fn = transform?.read || (node.tracked && transform?.read_tracked);
|
|
80
|
+
|
|
81
|
+
if (read_fn) {
|
|
82
|
+
return read_fn(node, context.state?.metadata?.spread, context.visit);
|
|
83
|
+
}
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
|
|
@@ -100,7 +105,7 @@ const visitors = {
|
|
|
100
105
|
const binding = context.state.scope.get(node.name);
|
|
101
106
|
if (
|
|
102
107
|
context.state.metadata?.tracking === false &&
|
|
103
|
-
is_tracked_name(node.name) &&
|
|
108
|
+
(is_tracked_name(node.name) || node.tracked) &&
|
|
104
109
|
binding?.node !== node
|
|
105
110
|
) {
|
|
106
111
|
context.state.metadata.tracking = true;
|
|
@@ -135,6 +140,18 @@ const visitors = {
|
|
|
135
140
|
context.state.metadata.tracking = true;
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
if (
|
|
144
|
+
!context.state.to_ts &&
|
|
145
|
+
callee.type === 'Identifier' &&
|
|
146
|
+
callee.name === 'tracked' &&
|
|
147
|
+
is_ripple_import(callee, context)
|
|
148
|
+
) {
|
|
149
|
+
return {
|
|
150
|
+
...node,
|
|
151
|
+
arguments: [...node.arguments.map((arg) => context.visit(arg)), b.id('__block')],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
138
155
|
if (
|
|
139
156
|
!is_inside_component(context, true) ||
|
|
140
157
|
context.state.to_ts ||
|
|
@@ -199,8 +216,8 @@ const visitors = {
|
|
|
199
216
|
callee.optional ? b.true : undefined,
|
|
200
217
|
node.optional ? b.true : undefined,
|
|
201
218
|
...node.arguments.map((arg) => context.visit(arg)),
|
|
202
|
-
)
|
|
203
|
-
)
|
|
219
|
+
),
|
|
220
|
+
),
|
|
204
221
|
);
|
|
205
222
|
}
|
|
206
223
|
}
|
|
@@ -508,9 +525,10 @@ const visitors = {
|
|
|
508
525
|
|
|
509
526
|
if (is_spreading) {
|
|
510
527
|
// For spread attributes, store just the actual value, not the full attribute string
|
|
511
|
-
const actual_value =
|
|
512
|
-
|
|
513
|
-
|
|
528
|
+
const actual_value =
|
|
529
|
+
is_boolean_attribute(name) && value === true
|
|
530
|
+
? b.literal(true)
|
|
531
|
+
: b.literal(value === true ? '' : value);
|
|
514
532
|
spread_attributes.push(b.prop('init', b.literal(name), actual_value));
|
|
515
533
|
} else {
|
|
516
534
|
state.template.push(attr_value);
|
|
@@ -665,9 +683,9 @@ const visitors = {
|
|
|
665
683
|
}
|
|
666
684
|
} else if (attr.type === 'SpreadAttribute') {
|
|
667
685
|
spread_attributes.push(b.spread(b.call('$.spread_object', visit(attr.argument, state))));
|
|
668
|
-
} else if (attr.type === '
|
|
686
|
+
} else if (attr.type === 'RefAttribute') {
|
|
669
687
|
const id = state.flush_node();
|
|
670
|
-
state.init.push(b.stmt(b.call('$.
|
|
688
|
+
state.init.push(b.stmt(b.call('$.ref', id, b.thunk(visit(attr.argument, state)))));
|
|
671
689
|
}
|
|
672
690
|
}
|
|
673
691
|
|
|
@@ -770,8 +788,8 @@ const visitors = {
|
|
|
770
788
|
),
|
|
771
789
|
),
|
|
772
790
|
);
|
|
773
|
-
} else if (attr.type === '
|
|
774
|
-
props.push(b.prop('init', b.call('$.
|
|
791
|
+
} else if (attr.type === 'RefAttribute') {
|
|
792
|
+
props.push(b.prop('init', b.call('$.ref_prop'), visit(attr.argument, state), true));
|
|
775
793
|
} else if (attr.type === 'AccessorAttribute') {
|
|
776
794
|
// # means it's an accessor to the runtime
|
|
777
795
|
tracked.push(b.literal('#' + attr.name.name));
|
|
@@ -1002,7 +1020,10 @@ const visitors = {
|
|
|
1002
1020
|
|
|
1003
1021
|
if (left.type === 'MemberExpression') {
|
|
1004
1022
|
// need to capture setting length of array to throw a runtime error
|
|
1005
|
-
if (
|
|
1023
|
+
if (
|
|
1024
|
+
left.property.type === 'Identifier' &&
|
|
1025
|
+
(is_tracked_name(left.property.name) || left.property.name === 'length')
|
|
1026
|
+
) {
|
|
1006
1027
|
if (left.property.name !== '$length') {
|
|
1007
1028
|
return b.call(
|
|
1008
1029
|
'$.set_property',
|
|
@@ -1058,8 +1079,9 @@ const visitors = {
|
|
|
1058
1079
|
const transformers = left && binding?.transform;
|
|
1059
1080
|
|
|
1060
1081
|
if (left === argument) {
|
|
1061
|
-
|
|
1062
|
-
|
|
1082
|
+
const update_fn = transformers?.update || transformers?.update_tracked;
|
|
1083
|
+
if (update_fn) {
|
|
1084
|
+
return update_fn(node);
|
|
1063
1085
|
}
|
|
1064
1086
|
}
|
|
1065
1087
|
|
|
@@ -1299,7 +1321,11 @@ const visitors = {
|
|
|
1299
1321
|
},
|
|
1300
1322
|
|
|
1301
1323
|
TemplateLiteral(node, context) {
|
|
1302
|
-
|
|
1324
|
+
if (node.expressions.length === 0) {
|
|
1325
|
+
return b.literal(node.quasis[0].value.cooked);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const expressions = node.expressions.map((expr) => context.visit(expr));
|
|
1303
1329
|
return b.template(node.quasis, expressions);
|
|
1304
1330
|
},
|
|
1305
1331
|
|
|
@@ -1409,12 +1435,12 @@ function transform_ts_child(node, context) {
|
|
|
1409
1435
|
const children = [];
|
|
1410
1436
|
let has_children_props = false;
|
|
1411
1437
|
|
|
1412
|
-
// Filter out
|
|
1413
|
-
const
|
|
1438
|
+
// Filter out RefAttributes and handle them separately
|
|
1439
|
+
const ref_attributes = [];
|
|
1414
1440
|
const attributes = node.attributes
|
|
1415
1441
|
.filter((attr) => {
|
|
1416
|
-
if (attr.type === '
|
|
1417
|
-
|
|
1442
|
+
if (attr.type === 'RefAttribute') {
|
|
1443
|
+
ref_attributes.push(attr);
|
|
1418
1444
|
return false;
|
|
1419
1445
|
}
|
|
1420
1446
|
return true;
|
|
@@ -1438,10 +1464,10 @@ function transform_ts_child(node, context) {
|
|
|
1438
1464
|
}
|
|
1439
1465
|
});
|
|
1440
1466
|
|
|
1441
|
-
// Add
|
|
1442
|
-
for (const
|
|
1467
|
+
// Add RefAttribute references separately for sourcemap purposes
|
|
1468
|
+
for (const ref_attr of ref_attributes) {
|
|
1443
1469
|
const metadata = { await: false };
|
|
1444
|
-
const argument = visit(
|
|
1470
|
+
const argument = visit(ref_attr.argument, { ...state, metadata });
|
|
1445
1471
|
state.init.push(b.stmt(argument));
|
|
1446
1472
|
}
|
|
1447
1473
|
|
|
@@ -61,8 +61,8 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
|
|
|
61
61
|
);
|
|
62
62
|
const source_content = source.substring(source_offset, source_offset + segment_length);
|
|
63
63
|
|
|
64
|
-
// Skip mappings for
|
|
65
|
-
if (source_content.includes('{
|
|
64
|
+
// Skip mappings for RefAttribute syntax to avoid overlapping sourcemaps
|
|
65
|
+
if (source_content.includes('{ref ') || source_content.match(/\{\s*ref\s+/)) {
|
|
66
66
|
continue;
|
|
67
67
|
}
|
|
68
68
|
|
package/src/compiler/utils.js
CHANGED
|
@@ -19,7 +19,7 @@ const VOID_ELEMENT_NAMES = [
|
|
|
19
19
|
'param',
|
|
20
20
|
'source',
|
|
21
21
|
'track',
|
|
22
|
-
'wbr'
|
|
22
|
+
'wbr',
|
|
23
23
|
];
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -564,19 +564,21 @@ export function build_assignment(operator, left, right, context) {
|
|
|
564
564
|
const transform = binding.transform;
|
|
565
565
|
|
|
566
566
|
// reassignment
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
);
|
|
567
|
+
if (object === left || (left.type === 'MemberExpression' && left.computed && operator === '=')) {
|
|
568
|
+
const assign_fn = transform?.assign || transform?.assign_tracked;
|
|
569
|
+
if (assign_fn) {
|
|
570
|
+
let value = /** @type {Expression} */ (
|
|
571
|
+
context.visit(build_assignment_value(operator, left, right))
|
|
572
|
+
);
|
|
574
573
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
574
|
+
return assign_fn(
|
|
575
|
+
object,
|
|
576
|
+
value,
|
|
577
|
+
left.type === 'MemberExpression' && left.computed
|
|
578
|
+
? context.visit(left.property)
|
|
579
|
+
: undefined,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
580
582
|
}
|
|
581
583
|
|
|
582
584
|
// mutation
|
package/src/runtime/array.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { TRACKED_OBJECT, ARRAY_SET_INDEX_AT } from './internal/client/constants.js';
|
|
1
|
+
import { TRACKED_OBJECT, ARRAY_SET_INDEX_AT, MAX_ARRAY_LENGTH } from './internal/client/constants.js';
|
|
2
2
|
import { get, safe_scope, set, tracked } from './internal/client/runtime.js';
|
|
3
3
|
import { is_ripple_array } from './internal/client/utils.js';
|
|
4
4
|
/** @import { Block, Tracked } from '#client' */
|
|
5
5
|
|
|
6
|
+
/** @type {unique symbol} */
|
|
7
|
+
const INIT_AFTER_NEW = Symbol();
|
|
8
|
+
|
|
6
9
|
/** @type {(symbol | string | any)[]} */
|
|
7
10
|
const introspect_methods = [
|
|
8
11
|
'concat',
|
|
@@ -36,7 +39,7 @@ const introspect_methods = [
|
|
|
36
39
|
'with',
|
|
37
40
|
];
|
|
38
41
|
|
|
39
|
-
let
|
|
42
|
+
let is_proto_set = false;
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
45
|
* @template T
|
|
@@ -45,6 +48,8 @@ let init = false;
|
|
|
45
48
|
export class RippleArray extends Array {
|
|
46
49
|
/** @type {Array<Tracked>} */
|
|
47
50
|
#tracked_elements = [];
|
|
51
|
+
/** @type {Tracked} */
|
|
52
|
+
// @ts-expect-error
|
|
48
53
|
#tracked_index;
|
|
49
54
|
|
|
50
55
|
/**
|
|
@@ -55,11 +60,11 @@ export class RippleArray extends Array {
|
|
|
55
60
|
* @returns {RippleArray<U>}
|
|
56
61
|
*/
|
|
57
62
|
static from(arrayLike, mapFn, thisArg) {
|
|
58
|
-
|
|
59
|
-
mapFn ?
|
|
63
|
+
var arr = mapFn ?
|
|
60
64
|
Array.from(arrayLike, mapFn, thisArg)
|
|
61
|
-
: Array.from(arrayLike)
|
|
62
|
-
|
|
65
|
+
: Array.from(arrayLike);
|
|
66
|
+
|
|
67
|
+
return get_instance_from_static(arr);
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
@@ -70,11 +75,30 @@ export class RippleArray extends Array {
|
|
|
70
75
|
* @returns {Promise<RippleArray<U>>}
|
|
71
76
|
*/
|
|
72
77
|
static async fromAsync(arrayLike, mapFn, thisArg) {
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
var block = safe_scope();
|
|
79
|
+
// create empty array to get the right scope
|
|
80
|
+
var result = new RippleArray();
|
|
81
|
+
|
|
82
|
+
var arr = mapFn ?
|
|
75
83
|
await Array.fromAsync(arrayLike, mapFn, thisArg)
|
|
76
84
|
: await Array.fromAsync(arrayLike)
|
|
77
|
-
|
|
85
|
+
|
|
86
|
+
var first = get_first_if_length(arr);
|
|
87
|
+
|
|
88
|
+
if (first) {
|
|
89
|
+
result[0] = first;
|
|
90
|
+
} else {
|
|
91
|
+
result.length = arr.length;
|
|
92
|
+
for (let i = 0; i < arr.length; i++) {
|
|
93
|
+
if (i in arr) {
|
|
94
|
+
result[i] = arr[i];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
result[INIT_AFTER_NEW](block);
|
|
100
|
+
|
|
101
|
+
return result
|
|
78
102
|
}
|
|
79
103
|
|
|
80
104
|
/**
|
|
@@ -83,7 +107,9 @@ export class RippleArray extends Array {
|
|
|
83
107
|
* @returns {RippleArray<U>}
|
|
84
108
|
*/
|
|
85
109
|
static of(...elements) {
|
|
86
|
-
|
|
110
|
+
var arr = Array.of(...elements);
|
|
111
|
+
|
|
112
|
+
return get_instance_from_static(arr);
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
/**
|
|
@@ -92,24 +118,33 @@ export class RippleArray extends Array {
|
|
|
92
118
|
constructor(...elements) {
|
|
93
119
|
super(...elements);
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
var tracked_elements = this.#tracked_elements;
|
|
121
|
+
this[INIT_AFTER_NEW]();
|
|
97
122
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
123
|
+
if (!is_proto_set) {
|
|
124
|
+
is_proto_set = true;
|
|
125
|
+
this.#set_proto();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
[INIT_AFTER_NEW](block = safe_scope()) {
|
|
130
|
+
if (this.length !== 0) {
|
|
131
|
+
var tracked_elements = this.#tracked_elements;
|
|
132
|
+
for (var i = 0; i < this.length; i++) {
|
|
133
|
+
if (!(i in this)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
tracked_elements[i] = tracked(this[i], block);
|
|
101
137
|
}
|
|
102
|
-
tracked_elements[i] = tracked(this[i], block);
|
|
103
138
|
}
|
|
104
|
-
this.#tracked_index = tracked(this.length, block);
|
|
105
139
|
|
|
106
|
-
if (!
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
if (!this.#tracked_index) {
|
|
141
|
+
this.#tracked_index = tracked(this.length, block);
|
|
142
|
+
} else if (this.#tracked_index.v !== this.length) {
|
|
143
|
+
set(this.#tracked_index, this.length, block);
|
|
109
144
|
}
|
|
110
145
|
}
|
|
111
146
|
|
|
112
|
-
#
|
|
147
|
+
#set_proto() {
|
|
113
148
|
var proto = RippleArray.prototype;
|
|
114
149
|
var array_proto = Array.prototype;
|
|
115
150
|
|
|
@@ -138,7 +173,7 @@ export class RippleArray extends Array {
|
|
|
138
173
|
// the caller reruns on length changes
|
|
139
174
|
this.$length;
|
|
140
175
|
// the caller reruns on element changes
|
|
141
|
-
|
|
176
|
+
establish_trackable_deps(this);
|
|
142
177
|
return result;
|
|
143
178
|
};
|
|
144
179
|
}
|
|
@@ -564,14 +599,72 @@ export class RippleArray extends Array {
|
|
|
564
599
|
export function get_all_elements(array) {
|
|
565
600
|
/** @type {Tracked[]} */
|
|
566
601
|
var tracked_elements = /** @type {Tracked[]} */ (array[TRACKED_OBJECT]);
|
|
567
|
-
|
|
602
|
+
// pre-allocate to support holey arrays
|
|
603
|
+
var result = new Array(array.length);
|
|
604
|
+
|
|
605
|
+
for (var i = 0; i < array.length; i++) {
|
|
606
|
+
if (tracked_elements[i] !== undefined) {
|
|
607
|
+
get(tracked_elements[i]);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (i in array) {
|
|
611
|
+
result[i] = array[i];
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* @template T
|
|
620
|
+
* @param {RippleArray<T>} array
|
|
621
|
+
* @returns {void}
|
|
622
|
+
*/
|
|
623
|
+
function establish_trackable_deps (array) {
|
|
624
|
+
var tracked_elements = array[TRACKED_OBJECT];
|
|
568
625
|
|
|
569
626
|
for (var i = 0; i < tracked_elements.length; i++) {
|
|
570
627
|
if (tracked_elements[i] !== undefined) {
|
|
571
628
|
get(tracked_elements[i]);
|
|
572
629
|
}
|
|
573
|
-
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* @template T
|
|
635
|
+
* @param {T[]} array
|
|
636
|
+
* @returns {RippleArray<T>}
|
|
637
|
+
*/
|
|
638
|
+
function get_instance_from_static(array) {
|
|
639
|
+
/** @type RippleArray<T> */
|
|
640
|
+
var result;
|
|
641
|
+
/** @type {T | void} */
|
|
642
|
+
var first = get_first_if_length(array);
|
|
643
|
+
|
|
644
|
+
if (first) {
|
|
645
|
+
result = new RippleArray();
|
|
646
|
+
result[0] = first;
|
|
647
|
+
result[INIT_AFTER_NEW]();
|
|
648
|
+
} else {
|
|
649
|
+
result = new RippleArray(...array);
|
|
574
650
|
}
|
|
575
651
|
|
|
576
|
-
return
|
|
652
|
+
return result;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* @template T
|
|
657
|
+
* @param {T[]} array
|
|
658
|
+
* @returns {T | void}
|
|
659
|
+
*/
|
|
660
|
+
function get_first_if_length (array) {
|
|
661
|
+
var first = array[0];
|
|
662
|
+
|
|
663
|
+
if (
|
|
664
|
+
array.length === 1 && (0 in array) && Number.isInteger(first)
|
|
665
|
+
&& /** @type {number} */ (first) >= 0
|
|
666
|
+
&& /** @type {number} */ (first) <= MAX_ARRAY_LENGTH
|
|
667
|
+
) {
|
|
668
|
+
return first;
|
|
669
|
+
}
|
|
577
670
|
}
|
package/src/runtime/index.js
CHANGED
|
@@ -36,7 +36,7 @@ export function mount(component, options) {
|
|
|
36
36
|
|
|
37
37
|
export { create_context as createContext } from './internal/client/context.js';
|
|
38
38
|
|
|
39
|
-
export { flush_sync as flushSync, untrack, deferred } from './internal/client/runtime.js';
|
|
39
|
+
export { flush_sync as flushSync, untrack, deferred, tracked } from './internal/client/runtime.js';
|
|
40
40
|
|
|
41
41
|
export { RippleArray } from './array.js';
|
|
42
42
|
|
|
@@ -50,3 +50,4 @@ export { user_effect as effect } from './internal/client/blocks.js';
|
|
|
50
50
|
|
|
51
51
|
export { Portal } from './internal/client/portal.js';
|
|
52
52
|
|
|
53
|
+
export { ref_prop as createRefKey } from './internal/client/runtime.js';
|
|
@@ -99,23 +99,23 @@ export function async(fn) {
|
|
|
99
99
|
* @param {() => (element: Element) => (void | (() => void))} get_fn
|
|
100
100
|
* @returns {Block}
|
|
101
101
|
*/
|
|
102
|
-
export function
|
|
102
|
+
export function ref(element, get_fn) {
|
|
103
103
|
/** @type {(element: Element) => (void | (() => void) | undefined)} */
|
|
104
|
-
var
|
|
104
|
+
var ref_fn;
|
|
105
105
|
/** @type {Block | null} */
|
|
106
106
|
var e;
|
|
107
107
|
|
|
108
108
|
return block(RENDER_BLOCK, () => {
|
|
109
|
-
if (
|
|
109
|
+
if (ref_fn !== (ref_fn = get_fn())) {
|
|
110
110
|
if (e) {
|
|
111
111
|
destroy_block(e);
|
|
112
112
|
e = null;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
if (
|
|
115
|
+
if (ref_fn) {
|
|
116
116
|
e = branch(() => {
|
|
117
117
|
effect(() => {
|
|
118
|
-
return
|
|
118
|
+
return ref_fn(element);
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
121
|
}
|
|
@@ -19,8 +19,11 @@ export var DESTROYED = 1 << 17;
|
|
|
19
19
|
export var LOGIC_BLOCK = FOR_BLOCK | IF_BLOCK | TRY_BLOCK;
|
|
20
20
|
|
|
21
21
|
export var UNINITIALIZED = Symbol();
|
|
22
|
-
|
|
22
|
+
/** @type {unique symbol} */
|
|
23
|
+
export const TRACKED_OBJECT = Symbol();
|
|
23
24
|
export var SPREAD_OBJECT = Symbol();
|
|
24
25
|
export var COMPUTED_PROPERTY = Symbol();
|
|
25
|
-
export var
|
|
26
|
-
|
|
26
|
+
export var REF_PROP = 'ref';
|
|
27
|
+
/** @type {unique symbol} */
|
|
28
|
+
export const ARRAY_SET_INDEX_AT = Symbol();
|
|
29
|
+
export const MAX_ARRAY_LENGTH = 2**32 - 1;
|
|
@@ -9,7 +9,7 @@ export {
|
|
|
9
9
|
set_selected,
|
|
10
10
|
} from './render.js';
|
|
11
11
|
|
|
12
|
-
export { render, render_spread, async,
|
|
12
|
+
export { render, render_spread, async, ref } from './blocks.js';
|
|
13
13
|
|
|
14
14
|
export { event, delegate } from './events.js';
|
|
15
15
|
|
|
@@ -42,7 +42,7 @@ export {
|
|
|
42
42
|
push_component,
|
|
43
43
|
pop_component,
|
|
44
44
|
untrack,
|
|
45
|
-
|
|
45
|
+
ref_prop,
|
|
46
46
|
fallback,
|
|
47
47
|
exclude_from_object,
|
|
48
48
|
} from './runtime.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { destroy_block,
|
|
2
|
-
import {
|
|
1
|
+
import { destroy_block, ref } from './blocks';
|
|
2
|
+
import { REF_PROP } from './constants';
|
|
3
3
|
import { get_descriptors, get_own_property_symbols, get_prototype_of } from './utils';
|
|
4
4
|
|
|
5
5
|
export function set_text(text, value) {
|
|
@@ -174,16 +174,16 @@ export function apply_element_spread(element, fn) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
for (const symbol of get_own_property_symbols(next)) {
|
|
177
|
-
var
|
|
177
|
+
var ref_fn = next[symbol];
|
|
178
178
|
|
|
179
|
-
if (symbol.description ===
|
|
179
|
+
if (symbol.description === REF_PROP && (!prev || ref_fn !== prev[symbol])) {
|
|
180
180
|
if (effects[symbol]) {
|
|
181
181
|
destroy_block(effects[symbol]);
|
|
182
182
|
}
|
|
183
|
-
effects[symbol] =
|
|
183
|
+
effects[symbol] = ref(element, () => ref_fn);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
next[symbol] =
|
|
186
|
+
next[symbol] = ref_fn;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
set_attributes(element, next);
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
TRACKED_OBJECT,
|
|
26
26
|
TRY_BLOCK,
|
|
27
27
|
UNINITIALIZED,
|
|
28
|
-
|
|
28
|
+
REF_PROP,
|
|
29
29
|
ARRAY_SET_INDEX_AT,
|
|
30
30
|
} from './constants';
|
|
31
31
|
import { capture, suspend } from './try.js';
|
|
@@ -260,6 +260,7 @@ export function run_block(block) {
|
|
|
260
260
|
* @returns {Tracked}
|
|
261
261
|
*/
|
|
262
262
|
export function tracked(v, b) {
|
|
263
|
+
// TODO: now we expose tracked, we should likely block access in DEV somehow
|
|
263
264
|
return {
|
|
264
265
|
b,
|
|
265
266
|
c: 0,
|
|
@@ -1203,8 +1204,8 @@ export function pop_component() {
|
|
|
1203
1204
|
active_component = component.p;
|
|
1204
1205
|
}
|
|
1205
1206
|
|
|
1206
|
-
export function
|
|
1207
|
-
return Symbol(
|
|
1207
|
+
export function ref_prop() {
|
|
1208
|
+
return Symbol(REF_PROP);
|
|
1208
1209
|
}
|
|
1209
1210
|
|
|
1210
1211
|
/**
|
package/tests/array.test.ripple
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { mount, flushSync, effect, untrack, RippleArray } from 'ripple';
|
|
3
|
-
import { ARRAY_SET_INDEX_AT } from '../src/runtime/internal/client/constants.js';
|
|
3
|
+
import { ARRAY_SET_INDEX_AT, MAX_ARRAY_LENGTH } from '../src/runtime/internal/client/constants.js';
|
|
4
4
|
|
|
5
5
|
describe('RippleArray', () => {
|
|
6
6
|
let container;
|
|
@@ -1244,37 +1244,34 @@ describe('RippleArray', () => {
|
|
|
1244
1244
|
expect(container.querySelector('pre').textContent).toBe('Cannot set length on RippleArray, use $length instead');
|
|
1245
1245
|
});
|
|
1246
1246
|
|
|
1247
|
-
('fromAsync' in Array.prototype ? describe : describe.skip)('RippleArray fromAsync', () => {
|
|
1247
|
+
('fromAsync' in Array.prototype ? describe : describe.skip)('RippleArray fromAsync', async () => {
|
|
1248
1248
|
it('handles static fromAsync method with reactivity', async () => {
|
|
1249
|
-
component
|
|
1250
|
-
let itemsPromise = RippleArray.fromAsync(Promise.resolve([1, 2, 3]));
|
|
1251
|
-
let items = null;
|
|
1252
|
-
let error = null;
|
|
1253
|
-
|
|
1249
|
+
component Parent() {
|
|
1254
1250
|
try {
|
|
1255
|
-
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1251
|
+
<ArrayTest />
|
|
1252
|
+
} async {
|
|
1253
|
+
<div>{'Loading placeholder...'}</div>
|
|
1258
1254
|
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
component ArrayTest() {
|
|
1258
|
+
let items = await RippleArray.fromAsync([1, 2, 3]);
|
|
1259
1259
|
|
|
1260
1260
|
<button onClick={() => {
|
|
1261
|
-
|
|
1261
|
+
if (items) items.push(4);
|
|
1262
1262
|
}}>{'add item'}</button>
|
|
1263
|
-
|
|
1264
|
-
<pre>{
|
|
1263
|
+
|
|
1264
|
+
<pre>{JSON.stringify(items)}</pre>
|
|
1265
1265
|
}
|
|
1266
1266
|
|
|
1267
|
-
render(
|
|
1267
|
+
render(Parent);
|
|
1268
1268
|
|
|
1269
|
-
|
|
1270
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1269
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1271
1270
|
flushSync();
|
|
1272
1271
|
|
|
1273
1272
|
const addButton = container.querySelector('button');
|
|
1274
1273
|
|
|
1275
|
-
|
|
1276
|
-
expect(container.querySelectorAll('pre')[0].textContent).toBe('Loaded');
|
|
1277
|
-
expect(container.querySelectorAll('pre')[1].textContent).toBe('[1,2,3]');
|
|
1274
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[1,2,3]');
|
|
1278
1275
|
|
|
1279
1276
|
// Test adding an item to the async-created array
|
|
1280
1277
|
addButton.click();
|
|
@@ -1284,33 +1281,35 @@ describe('RippleArray', () => {
|
|
|
1284
1281
|
});
|
|
1285
1282
|
|
|
1286
1283
|
it('handles static fromAsync method with mapping function', async () => {
|
|
1284
|
+
component Parent() {
|
|
1285
|
+
try {
|
|
1286
|
+
<ArrayTest />
|
|
1287
|
+
} async {
|
|
1288
|
+
<div>{'Loading placeholder...'}</div>
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1287
1292
|
component ArrayTest() {
|
|
1288
|
-
let
|
|
1289
|
-
|
|
1290
|
-
|
|
1293
|
+
let items = await RippleArray.fromAsync(
|
|
1294
|
+
[1, 2, 3],
|
|
1295
|
+
x => x * 2
|
|
1291
1296
|
);
|
|
1292
|
-
let items = null;
|
|
1293
|
-
|
|
1294
|
-
items = await itemsPromise;
|
|
1295
1297
|
|
|
1296
1298
|
<button onClick={() => {
|
|
1297
|
-
|
|
1299
|
+
if (items) items.push(8);
|
|
1298
1300
|
}}>{'add item'}</button>
|
|
1299
1301
|
<pre>{items ? JSON.stringify(items) : 'Loading...'}</pre>
|
|
1300
1302
|
}
|
|
1301
1303
|
|
|
1302
|
-
render(
|
|
1304
|
+
render(Parent);
|
|
1303
1305
|
|
|
1304
|
-
|
|
1305
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1306
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1306
1307
|
flushSync();
|
|
1307
1308
|
|
|
1308
1309
|
const addButton = container.querySelector('button');
|
|
1309
1310
|
|
|
1310
|
-
// Check that the promise resolved correctly with mapping applied
|
|
1311
1311
|
expect(container.querySelector('pre').textContent).toBe('[2,4,6]');
|
|
1312
1312
|
|
|
1313
|
-
// Test adding an item to the async-created array
|
|
1314
1313
|
addButton.click();
|
|
1315
1314
|
flushSync();
|
|
1316
1315
|
|
|
@@ -1318,13 +1317,20 @@ describe('RippleArray', () => {
|
|
|
1318
1317
|
});
|
|
1319
1318
|
|
|
1320
1319
|
it('handles error in fromAsync method', async () => {
|
|
1320
|
+
component Parent() {
|
|
1321
|
+
try {
|
|
1322
|
+
<ArrayTest />
|
|
1323
|
+
} async {
|
|
1324
|
+
<div>{'Loading placeholder...'}</div>
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1321
1328
|
component ArrayTest() {
|
|
1322
|
-
let itemsPromise = RippleArray.fromAsync(Promise.reject(new Error('Async error')));
|
|
1323
1329
|
let items = null;
|
|
1324
1330
|
let error = null;
|
|
1325
1331
|
|
|
1326
1332
|
try {
|
|
1327
|
-
items = await
|
|
1333
|
+
items = await RippleArray.fromAsync(Promise.reject(new Error('Async error')));
|
|
1328
1334
|
} catch (e) {
|
|
1329
1335
|
error = e.message;
|
|
1330
1336
|
}
|
|
@@ -1333,13 +1339,11 @@ describe('RippleArray', () => {
|
|
|
1333
1339
|
<pre>{items ? JSON.stringify(items) : 'No items'}</pre>
|
|
1334
1340
|
}
|
|
1335
1341
|
|
|
1336
|
-
render(
|
|
1342
|
+
render(Parent);
|
|
1337
1343
|
|
|
1338
|
-
|
|
1339
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1344
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1340
1345
|
flushSync();
|
|
1341
1346
|
|
|
1342
|
-
// Check that the error was caught correctly
|
|
1343
1347
|
expect(container.querySelectorAll('pre')[0].textContent).toBe('Error: Async error');
|
|
1344
1348
|
expect(container.querySelectorAll('pre')[1].textContent).toBe('No items');
|
|
1345
1349
|
});
|
|
@@ -1463,6 +1467,90 @@ describe('RippleArray', () => {
|
|
|
1463
1467
|
expect(container.querySelectorAll('pre')[5].textContent).toBe('items[4]: 4');
|
|
1464
1468
|
});
|
|
1465
1469
|
});
|
|
1470
|
+
|
|
1471
|
+
describe('Creates RippleArray with a single element', () => {
|
|
1472
|
+
it('specifies int', () => {
|
|
1473
|
+
component ArrayTest() {
|
|
1474
|
+
let items = new RippleArray(3);
|
|
1475
|
+
<pre>{JSON.stringify(items)}</pre>
|
|
1476
|
+
<pre>{items.$length}</pre>
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
render(ArrayTest);
|
|
1480
|
+
|
|
1481
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[null,null,null]');
|
|
1482
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
it('errors on exceeding max array size', () => {
|
|
1486
|
+
component ArrayTest() {
|
|
1487
|
+
let error = null;
|
|
1488
|
+
|
|
1489
|
+
try {
|
|
1490
|
+
new RippleArray(MAX_ARRAY_LENGTH + 1);
|
|
1491
|
+
} catch (e) {
|
|
1492
|
+
error = e.message;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
<pre>{error}</pre>
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
render(ArrayTest);
|
|
1499
|
+
|
|
1500
|
+
expect(container.querySelector('pre').textContent).toBe('Invalid array length');
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
it('specifies int using static from method', () => {
|
|
1504
|
+
component ArrayTest() {
|
|
1505
|
+
let items = RippleArray.from([4]);
|
|
1506
|
+
<pre>{JSON.stringify(items)}</pre>
|
|
1507
|
+
<pre>{items.$length}</pre>
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
render(ArrayTest);
|
|
1511
|
+
|
|
1512
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[4]');
|
|
1513
|
+
// expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
it('specifies int using static of method', () => {
|
|
1517
|
+
component ArrayTest() {
|
|
1518
|
+
let items = RippleArray.of(5);
|
|
1519
|
+
<pre>{JSON.stringify(items)}</pre>
|
|
1520
|
+
<pre>{items.$length}</pre>
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
render(ArrayTest);
|
|
1524
|
+
|
|
1525
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[5]');
|
|
1526
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
('fromAsync' in Array.prototype ? it : it.skip)('specifies int using static fromAsync method', async () => {
|
|
1530
|
+
component Parent() {
|
|
1531
|
+
try {
|
|
1532
|
+
<ArrayTest />
|
|
1533
|
+
} async {
|
|
1534
|
+
<div>{'Loading placeholder...'}</div>
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
component ArrayTest() {
|
|
1539
|
+
const items = await RippleArray.fromAsync([6]);
|
|
1540
|
+
|
|
1541
|
+
<pre>{items ? JSON.stringify(items) : 'Loading...'}</pre>
|
|
1542
|
+
<pre>{items ? items.$length : ''}</pre>
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
render(Parent);
|
|
1546
|
+
|
|
1547
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1548
|
+
flushSync();
|
|
1549
|
+
|
|
1550
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[6]');
|
|
1551
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
1552
|
+
});
|
|
1553
|
+
});
|
|
1466
1554
|
});
|
|
1467
1555
|
|
|
1468
1556
|
|
package/tests/basic.test.ripple
CHANGED
|
@@ -88,12 +88,39 @@ describe('basic', () => {
|
|
|
88
88
|
expect(container.querySelector('div').textContent).toEqual('');
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
it('render tick template literal for nested children', () => {
|
|
92
|
+
component Child({ $level, $children }) {
|
|
93
|
+
if($level == 1) {
|
|
94
|
+
<h1><$children /></h1>
|
|
95
|
+
}
|
|
96
|
+
if($level == 2) {
|
|
97
|
+
<h2><$children /></h2>
|
|
98
|
+
}
|
|
99
|
+
if($level == 3) {
|
|
100
|
+
<h3><$children /></h3>
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
component App() {
|
|
105
|
+
<Child $level={1}>{`Heading 1`} </Child>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
render(App);
|
|
109
|
+
expect(container.querySelector('h1').textContent).toEqual('Heading 1');
|
|
110
|
+
});
|
|
111
|
+
|
|
91
112
|
it('render dynamic class attribute', () => {
|
|
92
113
|
component Basic() {
|
|
93
114
|
let $active = false;
|
|
94
115
|
|
|
95
116
|
<button onClick={() => $active = !$active}>{'Toggle'}</button>
|
|
96
117
|
<div $class={$active ? 'active' : 'inactive'}>{'Dynamic Class'}</div>
|
|
118
|
+
|
|
119
|
+
<style>
|
|
120
|
+
.active {
|
|
121
|
+
color: green;
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
97
124
|
}
|
|
98
125
|
|
|
99
126
|
render(Basic);
|
|
@@ -101,17 +128,17 @@ describe('basic', () => {
|
|
|
101
128
|
const button = container.querySelector('button');
|
|
102
129
|
const div = container.querySelector('div');
|
|
103
130
|
|
|
104
|
-
expect(div.
|
|
105
|
-
|
|
131
|
+
expect(Array.from(div.classList).some(className => className.startsWith('ripple-'))).toBe(true);
|
|
132
|
+
expect(div.classList.contains('inactive')).toBe(true);
|
|
133
|
+
|
|
106
134
|
button.click();
|
|
107
135
|
flushSync();
|
|
108
|
-
|
|
109
|
-
expect(div.className).toBe('active');
|
|
136
|
+
expect(div.classList.contains('active')).toBe(true);
|
|
110
137
|
|
|
111
138
|
button.click();
|
|
112
139
|
flushSync();
|
|
113
140
|
|
|
114
|
-
expect(div.
|
|
141
|
+
expect(div.classList.contains('inactive')).toBe(true);
|
|
115
142
|
});
|
|
116
143
|
|
|
117
144
|
it('render dynamic id attribute', () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { mount, flushSync, RippleArray } from 'ripple';
|
|
3
3
|
|
|
4
|
-
describe('
|
|
4
|
+
describe('refs', () => {
|
|
5
5
|
let container;
|
|
6
6
|
|
|
7
7
|
function render(component) {
|
|
@@ -24,7 +24,7 @@ describe('@use element decorators', () => {
|
|
|
24
24
|
let div;
|
|
25
25
|
|
|
26
26
|
component Component() {
|
|
27
|
-
<div {
|
|
27
|
+
<div {ref (node) => { div = node; }}>{'Hello World'}</div>
|
|
28
28
|
}
|
|
29
29
|
render(Component);
|
|
30
30
|
flushSync();
|
|
@@ -37,11 +37,11 @@ describe('@use element decorators', () => {
|
|
|
37
37
|
component Component() {
|
|
38
38
|
let items = RippleArray.from([1, 2, 3]);
|
|
39
39
|
|
|
40
|
-
function
|
|
40
|
+
function componentRef(node) {
|
|
41
41
|
_node = node;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
<Child {
|
|
44
|
+
<Child {ref componentRef} {items} />
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
component Child(props) {
|