ripple 0.2.91 → 0.2.92
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/index.js +14 -8
- package/src/compiler/phases/1-parse/index.js +55 -7
- package/src/compiler/phases/3-transform/client/index.js +81 -49
- package/src/compiler/phases/3-transform/segments.js +57 -3
- package/src/compiler/scope.js +478 -404
- package/src/compiler/types/index.d.ts +299 -3
- package/src/compiler/utils.js +173 -30
- package/src/runtime/index-client.js +1 -0
- package/src/runtime/internal/client/html.js +18 -8
- package/src/runtime/internal/client/index.js +1 -0
- package/src/runtime/internal/client/portal.js +55 -32
- package/src/runtime/internal/client/render.js +31 -1
- package/src/runtime/internal/client/runtime.js +53 -22
- package/src/utils/normalize_css_property_name.js +23 -0
- package/tests/client/basic.test.ripple +207 -1
- package/tests/client/compiler.test.ripple +95 -1
- package/tests/client/html.test.ripple +29 -1
- package/tests/client/portal.test.ripple +167 -0
- package/types/index.d.ts +2 -0
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
get_own_property_symbols,
|
|
33
33
|
is_array,
|
|
34
34
|
is_tracked_object,
|
|
35
|
-
|
|
35
|
+
object_keys,
|
|
36
36
|
} from './utils.js';
|
|
37
37
|
|
|
38
38
|
const FLUSH_MICROTASK = 0;
|
|
@@ -46,6 +46,8 @@ export let active_reaction = null;
|
|
|
46
46
|
export let active_scope = null;
|
|
47
47
|
/** @type {null | Component} */
|
|
48
48
|
export let active_component = null;
|
|
49
|
+
/** @type {boolean} */
|
|
50
|
+
export let is_mutating_allowed = true;
|
|
49
51
|
|
|
50
52
|
/** @type {Map<Tracked, any>} */
|
|
51
53
|
var old_values = new Map();
|
|
@@ -170,6 +172,7 @@ function run_derived(computed) {
|
|
|
170
172
|
var previous_tracking = tracking;
|
|
171
173
|
var previous_dependency = active_dependency;
|
|
172
174
|
var previous_component = active_component;
|
|
175
|
+
var previous_is_mutating_allowed = is_mutating_allowed;
|
|
173
176
|
|
|
174
177
|
try {
|
|
175
178
|
active_block = computed.b;
|
|
@@ -177,6 +180,7 @@ function run_derived(computed) {
|
|
|
177
180
|
tracking = true;
|
|
178
181
|
active_dependency = null;
|
|
179
182
|
active_component = computed.co;
|
|
183
|
+
is_mutating_allowed = false;
|
|
180
184
|
|
|
181
185
|
destroy_computed_children(computed);
|
|
182
186
|
|
|
@@ -191,6 +195,7 @@ function run_derived(computed) {
|
|
|
191
195
|
tracking = previous_tracking;
|
|
192
196
|
active_dependency = previous_dependency;
|
|
193
197
|
active_component = previous_component;
|
|
198
|
+
is_mutating_allowed = previous_is_mutating_allowed;
|
|
194
199
|
}
|
|
195
200
|
}
|
|
196
201
|
|
|
@@ -407,7 +412,7 @@ function is_tracking_dirty(tracking) {
|
|
|
407
412
|
var tracked = tracking.t;
|
|
408
413
|
|
|
409
414
|
if ((tracked.f & DERIVED) !== 0) {
|
|
410
|
-
update_derived(/** @type {Derived} **/
|
|
415
|
+
update_derived(/** @type {Derived} **/(tracked));
|
|
411
416
|
}
|
|
412
417
|
|
|
413
418
|
if (tracked.c > tracking.c) {
|
|
@@ -467,7 +472,7 @@ export function async_computed(fn, block) {
|
|
|
467
472
|
}
|
|
468
473
|
|
|
469
474
|
promise.then((v) => {
|
|
470
|
-
if (parent && is_destroyed(/** @type {Block} */
|
|
475
|
+
if (parent && is_destroyed(/** @type {Block} */(parent))) {
|
|
471
476
|
return;
|
|
472
477
|
}
|
|
473
478
|
if (promise === current && t.v !== v) {
|
|
@@ -503,6 +508,21 @@ export function async_computed(fn, block) {
|
|
|
503
508
|
});
|
|
504
509
|
}
|
|
505
510
|
|
|
511
|
+
/**
|
|
512
|
+
* @template V
|
|
513
|
+
* @param {Function} fn
|
|
514
|
+
* @param {V} v
|
|
515
|
+
*/
|
|
516
|
+
function trigger_track_get(fn, v) {
|
|
517
|
+
var previous_is_mutating_allowed = is_mutating_allowed;
|
|
518
|
+
try {
|
|
519
|
+
is_mutating_allowed = false;
|
|
520
|
+
return untrack(() => fn(v));
|
|
521
|
+
} finally {
|
|
522
|
+
is_mutating_allowed = previous_is_mutating_allowed;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
506
526
|
/**
|
|
507
527
|
* @param {() => any} fn
|
|
508
528
|
* @returns {[any, Tracked[] | null]}
|
|
@@ -602,6 +622,13 @@ function flush_queued_root_blocks(root_blocks) {
|
|
|
602
622
|
}
|
|
603
623
|
}
|
|
604
624
|
|
|
625
|
+
/**
|
|
626
|
+
* @returns {Promise<void>}
|
|
627
|
+
*/
|
|
628
|
+
export async function tick() {
|
|
629
|
+
return new Promise((f) => requestAnimationFrame(() => f()));
|
|
630
|
+
}
|
|
631
|
+
|
|
605
632
|
/**
|
|
606
633
|
* @returns {void}
|
|
607
634
|
*/
|
|
@@ -702,8 +729,8 @@ export function get_derived(computed) {
|
|
|
702
729
|
register_dependency(computed);
|
|
703
730
|
}
|
|
704
731
|
var get = computed.a.get;
|
|
705
|
-
if (get) {
|
|
706
|
-
computed.v = get
|
|
732
|
+
if (get !== undefined) {
|
|
733
|
+
computed.v = trigger_track_get(get, computed.v);
|
|
707
734
|
}
|
|
708
735
|
|
|
709
736
|
return computed.v;
|
|
@@ -719,7 +746,7 @@ export function get(tracked) {
|
|
|
719
746
|
}
|
|
720
747
|
|
|
721
748
|
return (tracked.f & DERIVED) !== 0
|
|
722
|
-
? get_derived(/** @type {Derived} */
|
|
749
|
+
? get_derived(/** @type {Derived} */(tracked))
|
|
723
750
|
: get_tracked(tracked);
|
|
724
751
|
}
|
|
725
752
|
|
|
@@ -735,8 +762,8 @@ export function get_tracked(tracked) {
|
|
|
735
762
|
value = old_values.get(tracked);
|
|
736
763
|
}
|
|
737
764
|
var get = tracked.a.get;
|
|
738
|
-
if (get) {
|
|
739
|
-
value = get
|
|
765
|
+
if (get !== undefined) {
|
|
766
|
+
value = trigger_track_get(get, value);
|
|
740
767
|
}
|
|
741
768
|
return value;
|
|
742
769
|
}
|
|
@@ -747,6 +774,10 @@ export function get_tracked(tracked) {
|
|
|
747
774
|
* @param {Block} block
|
|
748
775
|
*/
|
|
749
776
|
export function set(tracked, value, block) {
|
|
777
|
+
if (!is_mutating_allowed) {
|
|
778
|
+
throw new Error('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
|
|
779
|
+
}
|
|
780
|
+
|
|
750
781
|
var old_value = tracked.v;
|
|
751
782
|
|
|
752
783
|
if (value !== old_value) {
|
|
@@ -760,9 +791,9 @@ export function set(tracked, value, block) {
|
|
|
760
791
|
}
|
|
761
792
|
}
|
|
762
793
|
|
|
763
|
-
|
|
764
|
-
if (set) {
|
|
765
|
-
value = set(value, old_value);
|
|
794
|
+
let set = tracked.a.set;
|
|
795
|
+
if (set !== undefined) {
|
|
796
|
+
value = untrack(() => set(value, old_value));
|
|
766
797
|
}
|
|
767
798
|
|
|
768
799
|
tracked.v = value;
|
|
@@ -838,10 +869,10 @@ export function spread_props(fn, block) {
|
|
|
838
869
|
const obj = get_derived(computed);
|
|
839
870
|
return obj[property];
|
|
840
871
|
},
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
872
|
+
has(target, property) {
|
|
873
|
+
const obj = get_derived(computed);
|
|
874
|
+
return property in obj;
|
|
875
|
+
},
|
|
845
876
|
ownKeys() {
|
|
846
877
|
const obj = get_derived(computed);
|
|
847
878
|
return Reflect.ownKeys(obj);
|
|
@@ -1086,15 +1117,15 @@ export function fallback(value, fallback) {
|
|
|
1086
1117
|
* @returns {Record<string | symbol, unknown>}
|
|
1087
1118
|
*/
|
|
1088
1119
|
export function exclude_from_object(obj, exclude_keys) {
|
|
1089
|
-
|
|
1120
|
+
var keys = object_keys(obj);
|
|
1090
1121
|
/** @type {Record<string | symbol, unknown>} */
|
|
1091
1122
|
var new_obj = {};
|
|
1092
1123
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1124
|
+
for (const key of keys) {
|
|
1125
|
+
if (!exclude_keys.includes(key)) {
|
|
1126
|
+
new_obj[key] = obj[key];
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1098
1129
|
|
|
1099
1130
|
for (const symbol of get_own_property_symbols(obj)) {
|
|
1100
1131
|
var ref_fn = obj[symbol];
|
|
@@ -1121,7 +1152,7 @@ export async function maybe_tracked(v) {
|
|
|
1121
1152
|
} else {
|
|
1122
1153
|
value = await async_computed(async () => {
|
|
1123
1154
|
return await get_tracked(v);
|
|
1124
|
-
}, /** @type {Block} */
|
|
1155
|
+
}, /** @type {Block} */(active_block));
|
|
1125
1156
|
}
|
|
1126
1157
|
} else {
|
|
1127
1158
|
value = await v;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @type {Map<string, string>} */
|
|
2
|
+
const normalized_properties_cache = new Map();
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Takes a camelCased string and returns a hyphenated string
|
|
6
|
+
* @param {string} str
|
|
7
|
+
* @returns {string}
|
|
8
|
+
* @example
|
|
9
|
+
* normalize_css_property_name('backgroundColor') // 'background-color'
|
|
10
|
+
*/
|
|
11
|
+
export function normalize_css_property_name(str) {
|
|
12
|
+
if (str.startsWith('--')) return str;
|
|
13
|
+
|
|
14
|
+
let normalized_result = normalized_properties_cache.get(str);
|
|
15
|
+
if (normalized_result != null) {
|
|
16
|
+
return normalized_result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
normalized_result = str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
20
|
+
normalized_properties_cache.set(str, normalized_result);
|
|
21
|
+
|
|
22
|
+
return normalized_result;
|
|
23
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mount, flushSync, effect, track, trackSplit } from 'ripple';
|
|
2
|
+
import { mount, flushSync, effect, track, trackSplit, untrack, tick } from 'ripple';
|
|
3
3
|
import { compile } from 'ripple/compiler';
|
|
4
4
|
import { TRACKED_ARRAY } from '../../src/runtime/internal/client/constants.js';
|
|
5
5
|
|
|
6
6
|
describe('basic client', () => {
|
|
7
7
|
let container;
|
|
8
|
+
let error;
|
|
8
9
|
|
|
9
10
|
function render(component) {
|
|
10
11
|
mount(component, {
|
|
@@ -15,11 +16,13 @@ describe('basic client', () => {
|
|
|
15
16
|
beforeEach(() => {
|
|
16
17
|
container = document.createElement('div');
|
|
17
18
|
document.body.appendChild(container);
|
|
19
|
+
error = undefined;
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
afterEach(() => {
|
|
21
23
|
document.body.removeChild(container);
|
|
22
24
|
container = null;
|
|
25
|
+
error = undefined;
|
|
23
26
|
});
|
|
24
27
|
|
|
25
28
|
it('render static text', () => {
|
|
@@ -266,6 +269,107 @@ describe('basic client', () => {
|
|
|
266
269
|
expect(div.style.fontWeight).toBe('bold');
|
|
267
270
|
});
|
|
268
271
|
|
|
272
|
+
it('render style attribute as dynamic object', () => {
|
|
273
|
+
component Basic() {
|
|
274
|
+
let color = track('red');
|
|
275
|
+
|
|
276
|
+
<button onClick={() => { @color = @color === 'red' ? 'blue' : 'red' }}>{'Change Color'}</button>
|
|
277
|
+
<div style={{
|
|
278
|
+
color: @color,
|
|
279
|
+
fontWeight: 'bold',
|
|
280
|
+
}}>{'Dynamic Style'}</div>
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
render(Basic);
|
|
284
|
+
|
|
285
|
+
const button = container.querySelector('button');
|
|
286
|
+
const div = container.querySelector('div');
|
|
287
|
+
|
|
288
|
+
expect(div.style.color).toBe('red');
|
|
289
|
+
expect(div.style.fontWeight).toBe('bold');
|
|
290
|
+
|
|
291
|
+
button.click();
|
|
292
|
+
flushSync();
|
|
293
|
+
|
|
294
|
+
expect(div.style.color).toBe('blue');
|
|
295
|
+
expect(div.style.fontWeight).toBe('bold');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('render tracked variable as style attribute', () => {
|
|
299
|
+
component Basic() {
|
|
300
|
+
let style = track({ color: 'red', fontWeight: 'bold' });
|
|
301
|
+
|
|
302
|
+
function toggleColor() {
|
|
303
|
+
@style = { ...@style, color: @style.color === 'red' ? 'blue' : 'red' };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
<button onClick={toggleColor}>{'Change Color'}</button>
|
|
307
|
+
<div style={@style}>{'Dynamic Style'}</div>
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
render(Basic);
|
|
311
|
+
|
|
312
|
+
const button = container.querySelector('button');
|
|
313
|
+
const div = container.querySelector('div');
|
|
314
|
+
|
|
315
|
+
expect(div.style.color).toBe('red');
|
|
316
|
+
expect(div.style.fontWeight).toBe('bold');
|
|
317
|
+
|
|
318
|
+
button.click();
|
|
319
|
+
flushSync();
|
|
320
|
+
|
|
321
|
+
expect(div.style.color).toBe('blue');
|
|
322
|
+
expect(div.style.fontWeight).toBe('bold');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('render tracked object as style attribute', () => {
|
|
326
|
+
component Basic() {
|
|
327
|
+
let style = #{ color: 'red', fontWeight: 'bold' };
|
|
328
|
+
|
|
329
|
+
function toggleColor() {
|
|
330
|
+
@style.color = @style.color === 'red' ? 'blue' : 'red';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
<button onClick={toggleColor}>{'Change Color'}</button>
|
|
334
|
+
<div style={{ ...@style }}>{'Dynamic Style'}</div>
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
render(Basic);
|
|
338
|
+
|
|
339
|
+
const button = container.querySelector('button');
|
|
340
|
+
const div = container.querySelector('div');
|
|
341
|
+
|
|
342
|
+
expect(div.style.color).toBe('red');
|
|
343
|
+
expect(div.style.fontWeight).toBe('bold');
|
|
344
|
+
|
|
345
|
+
button.click();
|
|
346
|
+
flushSync();
|
|
347
|
+
|
|
348
|
+
expect(div.style.color).toBe('blue');
|
|
349
|
+
expect(div.style.fontWeight).toBe('bold');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('render spread attributes with style and class', () => {
|
|
353
|
+
component Basic() {
|
|
354
|
+
const attributes = {
|
|
355
|
+
style: { color: 'red', fontWeight: 'bold' },
|
|
356
|
+
class: ['foo', false && 'bar'],
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
<div {...attributes}>{'Attributes with style and class'}</div>
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
render(Basic);
|
|
363
|
+
|
|
364
|
+
const div = container.querySelector('div');
|
|
365
|
+
|
|
366
|
+
expect(div.style.color).toBe('red');
|
|
367
|
+
expect(div.style.fontWeight).toBe('bold');
|
|
368
|
+
|
|
369
|
+
expect(div.classList.contains('foo')).toBe(true);
|
|
370
|
+
expect(div.classList.contains('bar')).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
269
373
|
it('render spread props without duplication', () => {
|
|
270
374
|
component App() {
|
|
271
375
|
const checkBoxProp = {name:'car'}
|
|
@@ -1637,5 +1741,107 @@ describe('basic client', () => {
|
|
|
1637
1741
|
render(App);
|
|
1638
1742
|
expect(container).toMatchSnapshot();
|
|
1639
1743
|
});
|
|
1744
|
+
|
|
1745
|
+
it('tick function', async () => {
|
|
1746
|
+
let resolve;
|
|
1747
|
+
const promise = new Promise((res) => (resolve = res));
|
|
1748
|
+
|
|
1749
|
+
component Basic() {
|
|
1750
|
+
let value = track(0);
|
|
1751
|
+
effect(() => {
|
|
1752
|
+
untrack(() => {
|
|
1753
|
+
@value++;
|
|
1754
|
+
tick().then(() => resolve());
|
|
1755
|
+
});
|
|
1756
|
+
});
|
|
1757
|
+
<p>{@value}</p>
|
|
1758
|
+
}
|
|
1759
|
+
render(Basic);
|
|
1760
|
+
|
|
1761
|
+
const p = container.querySelector('p');
|
|
1762
|
+
expect(p.textContent).toBe('0');
|
|
1763
|
+
await promise;
|
|
1764
|
+
expect(p.textContent).toBe('1');
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1767
|
+
it('errors on mutating tracked value inside computed track() evaluation', () => {
|
|
1768
|
+
component Basic() {
|
|
1769
|
+
let count = track(0);
|
|
1770
|
+
|
|
1771
|
+
const doubled = track(() => {
|
|
1772
|
+
try {
|
|
1773
|
+
@count *= 2;
|
|
1774
|
+
} catch (e) {
|
|
1775
|
+
error = e.message;
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
<p>{@doubled}</p>
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
render(Basic);
|
|
1783
|
+
|
|
1784
|
+
expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
it('errors on mutating tracked value inside untrack() in computed track() evaluation', () => {
|
|
1788
|
+
component Basic() {
|
|
1789
|
+
let count = track(0);
|
|
1790
|
+
|
|
1791
|
+
const doubled = track(() => {
|
|
1792
|
+
try {
|
|
1793
|
+
untrack(() => {
|
|
1794
|
+
@count *= 2;
|
|
1795
|
+
});
|
|
1796
|
+
} catch (e) {
|
|
1797
|
+
error = e.message;
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
<p>{@doubled}</p>
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
render(Basic);
|
|
1805
|
+
|
|
1806
|
+
expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
it("errors on mutating a tracked variable in track() getter", () => {
|
|
1810
|
+
component Basic() {
|
|
1811
|
+
let count = track(0);
|
|
1812
|
+
|
|
1813
|
+
const doubled = track(0, (value) => {
|
|
1814
|
+
try {
|
|
1815
|
+
@count += 1;
|
|
1816
|
+
} catch (e) {
|
|
1817
|
+
error = e.message;
|
|
1818
|
+
}
|
|
1819
|
+
return value;
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1822
|
+
<p>{@doubled}</p>
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
render(Basic);
|
|
1826
|
+
|
|
1827
|
+
expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
it("doesn't error on mutating a tracked variable in track() setter", () => {
|
|
1831
|
+
component Basic() {
|
|
1832
|
+
let count = track(0);
|
|
1833
|
+
|
|
1834
|
+
const doubled = track(0, undefined, (value) => {
|
|
1835
|
+
@count += value;
|
|
1836
|
+
return value;
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
<p>{@doubled}</p>
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
render(Basic);
|
|
1843
|
+
|
|
1844
|
+
expect(error).toBe(undefined);
|
|
1845
|
+
});
|
|
1640
1846
|
});
|
|
1641
1847
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { mount, TrackedArray, track } from 'ripple';
|
|
3
|
-
import { parse } from 'ripple/compiler'
|
|
3
|
+
import { parse, compile } from 'ripple/compiler'
|
|
4
4
|
|
|
5
5
|
describe('compiler success tests', () => {
|
|
6
6
|
let container;
|
|
@@ -280,4 +280,98 @@ describe('compiler success tests', () => {
|
|
|
280
280
|
|
|
281
281
|
render(App);
|
|
282
282
|
});
|
|
283
|
+
|
|
284
|
+
describe('attribute name handling', () => {
|
|
285
|
+
it('generates valid JavaScript for component props with hyphenated attributes', () => {
|
|
286
|
+
const source = `
|
|
287
|
+
component Child(props) {
|
|
288
|
+
<div />
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export default component App() {
|
|
292
|
+
<Child data-scope="test" aria-label="accessible" class="valid" />
|
|
293
|
+
}`;
|
|
294
|
+
|
|
295
|
+
const result = compile(source, 'test.ripple', { mode: 'client' });
|
|
296
|
+
|
|
297
|
+
// Should contain properly quoted hyphenated properties and unquoted valid identifiers
|
|
298
|
+
expect(result.js.code).toMatch(/'data-scope': "test"/);
|
|
299
|
+
expect(result.js.code).toMatch(/'aria-label': "accessible"/);
|
|
300
|
+
expect(result.js.code).toMatch(/class: "valid"/);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('generates valid JavaScript for all types of hyphenated attributes', () => {
|
|
304
|
+
const testCases = [
|
|
305
|
+
{ attr: 'data-testid="value"', expected: /'data-testid': "value"/ },
|
|
306
|
+
{ attr: 'aria-label="label"', expected: /'aria-label': "label"/ },
|
|
307
|
+
{ attr: 'data-custom-attr="custom"', expected: /'data-custom-attr': "custom"/ },
|
|
308
|
+
{ attr: 'ng-if="condition"', expected: /'ng-if': "condition"/ },
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
testCases.forEach(({ attr, expected }) => {
|
|
312
|
+
const source = `
|
|
313
|
+
component Child(props) { <div /> }
|
|
314
|
+
export default component App() { <Child ${attr} /> }`;
|
|
315
|
+
|
|
316
|
+
const result = compile(source, 'test.ripple', { mode: 'client' });
|
|
317
|
+
expect(result.js.code).toMatch(expected);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('handles mixed valid and invalid attribute identifiers correctly', () => {
|
|
322
|
+
const source = `
|
|
323
|
+
component Child(props) {
|
|
324
|
+
<div />
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export default component App() {
|
|
328
|
+
<Child
|
|
329
|
+
validProp="valid"
|
|
330
|
+
class="valid"
|
|
331
|
+
id="valid"
|
|
332
|
+
data-invalid="invalid"
|
|
333
|
+
aria-invalid="invalid"
|
|
334
|
+
custom-prop="invalid"
|
|
335
|
+
/>
|
|
336
|
+
}`;
|
|
337
|
+
|
|
338
|
+
const result = compile(source, 'test.ripple', { mode: 'client' });
|
|
339
|
+
|
|
340
|
+
// Valid identifiers should not be quoted
|
|
341
|
+
expect(result.js.code).toMatch(/validProp: "valid"/);
|
|
342
|
+
expect(result.js.code).toMatch(/class: "valid"/);
|
|
343
|
+
expect(result.js.code).toMatch(/id: "valid"/);
|
|
344
|
+
|
|
345
|
+
// Invalid identifiers (with hyphens) should be quoted
|
|
346
|
+
expect(result.js.code).toMatch(/'data-invalid': "invalid"/);
|
|
347
|
+
expect(result.js.code).toMatch(/'aria-invalid': "invalid"/);
|
|
348
|
+
expect(result.js.code).toMatch(/'custom-prop': "invalid"/);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('ensures generated code is syntactically valid JavaScript', () => {
|
|
352
|
+
const source = `
|
|
353
|
+
component Child(props) {
|
|
354
|
+
<div />
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export default component App() {
|
|
358
|
+
<Child data-scope="test" />
|
|
359
|
+
}`;
|
|
360
|
+
|
|
361
|
+
const result = compile(source, 'test.ripple', { mode: 'client' });
|
|
362
|
+
|
|
363
|
+
// Extract the props object from the generated code and test it's valid JavaScript
|
|
364
|
+
const match = result.js.code.match(/Child\([^,]+,\s*(\{[^}]+\})/);
|
|
365
|
+
expect(match).toBeTruthy();
|
|
366
|
+
|
|
367
|
+
const propsObject = match[1];
|
|
368
|
+
expect(() => {
|
|
369
|
+
// Test that the object literal is syntactically valid
|
|
370
|
+
new Function(`return ${propsObject}`);
|
|
371
|
+
}).not.toThrow();
|
|
372
|
+
|
|
373
|
+
// Also verify it contains the expected quoted property
|
|
374
|
+
expect(propsObject).toMatch(/'data-scope': "test"/);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
283
377
|
});
|
|
@@ -49,4 +49,32 @@ describe('html directive', () => {
|
|
|
49
49
|
|
|
50
50
|
expect(container).toMatchSnapshot();
|
|
51
51
|
});
|
|
52
|
-
|
|
52
|
+
|
|
53
|
+
it('renders the correct namespace for child svg element when html is surrounded by <svg>', () => {
|
|
54
|
+
component App() {
|
|
55
|
+
let str = '<circle r="45" cx="50" cy="50" fill="red" />';
|
|
56
|
+
|
|
57
|
+
<svg height="100" width="100">{html str}</svg>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
render(App);
|
|
61
|
+
|
|
62
|
+
const circle = container.querySelector('circle');
|
|
63
|
+
expect(circle.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders the correct namespace for child math element when html is surrounded by <math>', () => {
|
|
67
|
+
component App() {
|
|
68
|
+
let str = '<mi>x</mi><mo>+</mo><mi>y</mi>';
|
|
69
|
+
|
|
70
|
+
<math>{html str}</math>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
render(App);
|
|
74
|
+
|
|
75
|
+
const mi = container.querySelector('mi');
|
|
76
|
+
const mo = container.querySelector('mo');
|
|
77
|
+
expect(mi.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
|
|
78
|
+
expect(mo.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
|
|
79
|
+
});
|
|
80
|
+
});
|