ripple 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/package.json +3 -3
- package/src/constants.js +6 -0
- package/src/runtime/internal/client/attributes.js +251 -125
- package/src/runtime/internal/client/blocks.js +16 -6
- package/src/runtime/internal/client/composite.js +292 -100
- package/src/runtime/internal/client/constants.js +8 -0
- package/src/runtime/internal/client/expression.js +4 -4
- package/src/runtime/internal/client/for.js +13 -38
- package/src/runtime/internal/client/if.js +56 -1
- package/src/runtime/internal/client/index.js +6 -6
- package/src/runtime/internal/client/runtime.js +73 -11
- package/src/runtime/internal/client/template-ns.js +45 -4
- package/src/runtime/internal/client/template.js +56 -0
- package/tests/client/composite/composite.dynamic-components.test.tsrx +40 -0
- package/tests/client/css/scoped-styles.test.tsrx +3 -1
- package/tests/client/svg.test.tsrx +273 -1
- package/tests/hydration/compiled/client/basic.js +36 -35
- package/tests/hydration/compiled/client/composite.js +11 -2
- package/tests/hydration/compiled/client/events.js +1 -1
- package/tests/hydration/compiled/client/for.js +73 -62
- package/tests/hydration/compiled/client/fragment-cursor.js +182 -115
- package/tests/hydration/compiled/client/head.js +10 -10
- package/tests/hydration/compiled/client/hmr.js +11 -2
- package/tests/hydration/compiled/client/html-in-template.js +1 -1
- package/tests/hydration/compiled/client/html.js +78 -50
- package/tests/hydration/compiled/client/if-children.js +94 -26
- package/tests/hydration/compiled/client/if-fragment-controlflow.js +34 -14
- package/tests/hydration/compiled/client/if.js +65 -17
- package/tests/hydration/compiled/client/mixed-control-flow.js +50 -20
- package/tests/hydration/compiled/client/nested-control-flow.js +188 -97
- package/tests/hydration/compiled/client/portal.js +14 -5
- package/tests/hydration/compiled/client/reactivity.js +3 -3
- package/tests/hydration/compiled/client/streaming.js +17 -17
- package/tests/hydration/compiled/client/switch.js +17 -17
- package/tests/hydration/compiled/client/track-async-serialization.js +19 -19
- package/tests/hydration/compiled/client/try.js +9 -9
- package/tests/hydration/compiled/client/typed-text.js +3 -2
|
@@ -27,10 +27,13 @@ import {
|
|
|
27
27
|
SCHEDULED,
|
|
28
28
|
SELECTOR,
|
|
29
29
|
IF_BLOCK,
|
|
30
|
+
COMPOSITE_BLOCK,
|
|
30
31
|
ITEM_BLOCK,
|
|
31
32
|
RELEASED,
|
|
32
33
|
RENDER_ENTRY,
|
|
33
34
|
CREATES_DERIVEDS,
|
|
35
|
+
NAMESPACE_BLOCK,
|
|
36
|
+
SVG_BLOCK,
|
|
34
37
|
} from './constants.js';
|
|
35
38
|
import { is_ripple_object } from './utils.js';
|
|
36
39
|
import { scope_orphan, set_in_derived, track_orphan, update_depth_exceeded } from './errors.js';
|
|
@@ -367,9 +370,10 @@ function handle_run_error(error, block) {
|
|
|
367
370
|
* @returns {Derived[] | null}
|
|
368
371
|
*/
|
|
369
372
|
function prepare_rerun(block) {
|
|
370
|
-
// A list, an if, and a list item re-run only their own logic;
|
|
371
|
-
// blocks (items, the branch's blocks,
|
|
372
|
-
|
|
373
|
+
// A list, an if, a composite and a list item re-run only their own logic;
|
|
374
|
+
// their child blocks (items, the branch's blocks, a dynamic element's
|
|
375
|
+
// children, an item body's nested blocks) stay.
|
|
376
|
+
if ((block.f & (FOR_BLOCK | IF_BLOCK | ITEM_BLOCK | COMPOSITE_BLOCK)) === 0) {
|
|
373
377
|
destroy_non_branch_children(block);
|
|
374
378
|
}
|
|
375
379
|
run_teardown(block);
|
|
@@ -393,17 +397,18 @@ function register_teardown(block, teardown) {
|
|
|
393
397
|
|
|
394
398
|
/**
|
|
395
399
|
* Runs the body of a branch block that was just created, in place of a first
|
|
396
|
-
* `run_block`: a branch renders untracked
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
* `run_block` handles them.
|
|
400
|
+
* `run_block`: a branch renders untracked, so the only bookkeeping its first
|
|
401
|
+
* run needs is the globals the body reads and the dependencies a nested
|
|
402
|
+
* `item` records on it (a list item re-runs through `run_block`, see
|
|
403
|
+
* `run_item`). Errors are handled as `run_block` handles them.
|
|
400
404
|
* @param {Block} block
|
|
401
405
|
* @param {(anchor: any, value: any, index?: any, key?: any) => void} fn
|
|
402
406
|
* @param {any} anchor
|
|
403
|
-
* @param {any} value
|
|
404
|
-
* @param {any} [
|
|
407
|
+
* @param {any} value a list item's value, or its tracked
|
|
408
|
+
* @param {any} [index] a list item's tracked index (see `create_item`)
|
|
409
|
+
* @param {any} [key] a keyed list item's key
|
|
405
410
|
*/
|
|
406
|
-
export function run_branch(block, fn, anchor, value, key) {
|
|
411
|
+
export function run_branch(block, fn, anchor, value, index, key) {
|
|
407
412
|
var previous_block = active_block;
|
|
408
413
|
var previous_reaction = active_reaction;
|
|
409
414
|
var previous_tracking = tracking;
|
|
@@ -416,7 +421,7 @@ export function run_branch(block, fn, anchor, value, key) {
|
|
|
416
421
|
active_component = block.co;
|
|
417
422
|
tracking = false;
|
|
418
423
|
active_dependency = null;
|
|
419
|
-
fn(anchor, value,
|
|
424
|
+
fn(anchor, value, index, key);
|
|
420
425
|
if (active_dependency !== null) {
|
|
421
426
|
block.d = active_dependency;
|
|
422
427
|
}
|
|
@@ -431,6 +436,43 @@ export function run_branch(block, fn, anchor, value, key) {
|
|
|
431
436
|
}
|
|
432
437
|
}
|
|
433
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Runs `fn(block.s, arg)` as a rerun of `block` driven from outside it: an if
|
|
441
|
+
* whose condition the enclosing render function evaluates renders its branch
|
|
442
|
+
* this way. The function runs under the block, in the block's namespace, and
|
|
443
|
+
* the deriveds the block's previous run created are released afterwards
|
|
444
|
+
* unless still read, as `run_block` does for a rerun. An error propagates to
|
|
445
|
+
* the caller's block.
|
|
446
|
+
* @template S, A
|
|
447
|
+
* @param {Block} block
|
|
448
|
+
* @param {(state: S, arg: A) => void} fn
|
|
449
|
+
* @param {A} arg
|
|
450
|
+
*/
|
|
451
|
+
export function run_in_block(block, fn, arg) {
|
|
452
|
+
var previous_block = active_block;
|
|
453
|
+
var previous_reaction = active_reaction;
|
|
454
|
+
var previous_component = active_component;
|
|
455
|
+
var previous_namespace = active_namespace;
|
|
456
|
+
var previous_deriveds = (block.f & CREATES_DERIVEDS) !== 0 ? take_created_deriveds(block) : null;
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
active_block = block;
|
|
460
|
+
active_reaction = block;
|
|
461
|
+
active_component = block.co;
|
|
462
|
+
var ns = block.f & NAMESPACE_BLOCK;
|
|
463
|
+
active_namespace = ns === 0 ? DEFAULT_NAMESPACE : ns === SVG_BLOCK ? 'svg' : 'mathml';
|
|
464
|
+
fn(/** @type {S} */ (block.s), arg);
|
|
465
|
+
} finally {
|
|
466
|
+
active_block = previous_block;
|
|
467
|
+
active_reaction = previous_reaction;
|
|
468
|
+
active_component = previous_component;
|
|
469
|
+
active_namespace = previous_namespace;
|
|
470
|
+
if (previous_deriveds !== null) {
|
|
471
|
+
keep_deriveds(block, release_deriveds(previous_deriveds));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
434
476
|
/**
|
|
435
477
|
* @param {Block} block
|
|
436
478
|
* @param {boolean} [first_run] true when the block has no children, teardown,
|
|
@@ -442,6 +484,7 @@ export function run_block(block, first_run = false) {
|
|
|
442
484
|
var previous_tracking = tracking;
|
|
443
485
|
var previous_dependency = active_dependency;
|
|
444
486
|
var previous_component = active_component;
|
|
487
|
+
var previous_namespace = active_namespace;
|
|
445
488
|
/** @type {Derived[] | null} */
|
|
446
489
|
var previous_deriveds = null;
|
|
447
490
|
|
|
@@ -449,6 +492,10 @@ export function run_block(block, first_run = false) {
|
|
|
449
492
|
active_block = block;
|
|
450
493
|
active_reaction = block;
|
|
451
494
|
active_component = block.co;
|
|
495
|
+
// The namespace the block was created in (see `create_block`): a rerun
|
|
496
|
+
// from a flush is outside the `with_ns()` call that established it.
|
|
497
|
+
var ns = block.f & NAMESPACE_BLOCK;
|
|
498
|
+
active_namespace = ns === 0 ? DEFAULT_NAMESPACE : ns === SVG_BLOCK ? 'svg' : 'mathml';
|
|
452
499
|
|
|
453
500
|
if (!first_run) {
|
|
454
501
|
previous_deriveds = prepare_rerun(block);
|
|
@@ -478,6 +525,7 @@ export function run_block(block, first_run = false) {
|
|
|
478
525
|
tracking = previous_tracking;
|
|
479
526
|
active_dependency = previous_dependency;
|
|
480
527
|
active_component = previous_component;
|
|
528
|
+
active_namespace = previous_namespace;
|
|
481
529
|
|
|
482
530
|
// The previous run's deriveds, whether this run finished or threw, and
|
|
483
531
|
// after this run's dependencies replaced the old ones (so a derived the
|
|
@@ -1827,6 +1875,20 @@ export function with_ns(namespace, fn) {
|
|
|
1827
1875
|
}
|
|
1828
1876
|
}
|
|
1829
1877
|
|
|
1878
|
+
/**
|
|
1879
|
+
* Sets the active namespace and returns the previous one, for setup code that
|
|
1880
|
+
* switches namespace part-way through a render function (the children of a
|
|
1881
|
+
* `foreignObject`), where a `with_ns()` thunk would hide its declarations.
|
|
1882
|
+
* `run_block` restores the namespace after the render function either way.
|
|
1883
|
+
* @param {keyof typeof NAMESPACE_URI} namespace
|
|
1884
|
+
* @returns {keyof typeof NAMESPACE_URI}
|
|
1885
|
+
*/
|
|
1886
|
+
export function set_ns(namespace) {
|
|
1887
|
+
var previous_namespace = active_namespace;
|
|
1888
|
+
active_namespace = namespace;
|
|
1889
|
+
return previous_namespace;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1830
1892
|
/**
|
|
1831
1893
|
* @returns {symbol}
|
|
1832
1894
|
*/
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
/** @import { NAMESPACE_URI } from './constants.js' */
|
|
1
2
|
import { get_first_child } from './operations.js';
|
|
3
|
+
import { render_component, set_ns, with_ns as run_with_ns } from './runtime.js';
|
|
2
4
|
import { set_ns_parser, template } from './template.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
@@ -26,12 +28,20 @@ function from_namespace(content, ns) {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* Runs `fn` with `namespace` active: content rendered inside it (children
|
|
32
|
+
* passed into an `<svg>`, a component rendered there, a dynamic element's
|
|
33
|
+
* children) creates its DOM in that namespace, and every block created
|
|
34
|
+
* inside records it (see `create_block`) so its reruns do too. Templates
|
|
35
|
+
* cloned in the namespace parse through `from_namespace`, so it is installed
|
|
36
|
+
* here: the compiler only emits this call for the SVG and MathML namespaces.
|
|
37
|
+
* @template T
|
|
38
|
+
* @param {keyof typeof NAMESPACE_URI} namespace
|
|
39
|
+
* @param {() => T} fn
|
|
40
|
+
* @returns {T}
|
|
32
41
|
*/
|
|
33
|
-
export function
|
|
42
|
+
export function with_ns(namespace, fn) {
|
|
34
43
|
set_ns_parser(from_namespace);
|
|
44
|
+
return run_with_ns(namespace, fn);
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
/**
|
|
@@ -45,3 +55,34 @@ export function template_ns(content, flags, count) {
|
|
|
45
55
|
set_ns_parser(from_namespace);
|
|
46
56
|
return template(content, flags, count);
|
|
47
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Enters `namespace` for the rest of a setup function, as `with_ns` does for
|
|
61
|
+
* a thunk: the compiler emits this where the content that follows is a
|
|
62
|
+
* dynamic element or a component called inside an `<svg>` or `<math>`
|
|
63
|
+
* template. Returns the namespace to restore with `set_ns`.
|
|
64
|
+
* @param {keyof typeof NAMESPACE_URI} namespace
|
|
65
|
+
* @returns {keyof typeof NAMESPACE_URI}
|
|
66
|
+
*/
|
|
67
|
+
export function push_ns(namespace) {
|
|
68
|
+
set_ns_parser(from_namespace);
|
|
69
|
+
return set_ns(namespace);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `render_component` for a component called inside an `<svg>` or `<math>`
|
|
74
|
+
* template: its content renders in that namespace.
|
|
75
|
+
* @param {keyof typeof NAMESPACE_URI} namespace
|
|
76
|
+
* @param {Function} fn
|
|
77
|
+
* @param {Node | import('#client').AppendIntoAnchor} anchor
|
|
78
|
+
* @param {Record<string, any>} props
|
|
79
|
+
* @returns {void}
|
|
80
|
+
*/
|
|
81
|
+
export function render_component_ns(namespace, fn, anchor, props) {
|
|
82
|
+
var previous = push_ns(namespace);
|
|
83
|
+
try {
|
|
84
|
+
render_component(fn, anchor, props);
|
|
85
|
+
} finally {
|
|
86
|
+
set_ns(previous);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { H, hydrate_node, hydrating } from './hydration.js';
|
|
10
10
|
import { create_text, get_first_child, is_firefox } from './operations.js';
|
|
11
11
|
import { active_block, active_namespace } from './runtime.js';
|
|
12
|
+
import { DEFAULT_NAMESPACE, NAMESPACE_URI } from './constants.js';
|
|
12
13
|
import { HYDRATION } from 'ripple/internal/client/hydration-enabled';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -142,6 +143,61 @@ export function template(content, flags = 0, count = 1) {
|
|
|
142
143
|
: clone_template(t);
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The document that owns parsed template content: it has no browsing
|
|
148
|
+
* context, so an element created in it does not load its `src` (an image,
|
|
149
|
+
* a video) until a clone is adopted into the page, exactly like a node
|
|
150
|
+
* parsed from a `<template>`.
|
|
151
|
+
* @type {Document | undefined}
|
|
152
|
+
*/
|
|
153
|
+
var inert_document;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A template that is one HTML element with static attributes and at most one
|
|
157
|
+
* text child, built with DOM calls instead of parsed: a `<template>` parse
|
|
158
|
+
* has a fixed cost that dwarfs the element itself, and an app's first render
|
|
159
|
+
* pays it once per distinct template. The compiler decides which templates
|
|
160
|
+
* qualify (`template_el` in the client transform); every other shape still
|
|
161
|
+
* parses. The element is created in the inert template document and in the
|
|
162
|
+
* active namespace, as a parsed template would be.
|
|
163
|
+
* @param {string} tag
|
|
164
|
+
* @param {string[] | null} [attributes] - flat name/value pairs
|
|
165
|
+
* @param {string} [text]
|
|
166
|
+
* @returns {() => Node}
|
|
167
|
+
*/
|
|
168
|
+
export function template_el(tag, attributes = null, text = '') {
|
|
169
|
+
/** @type {Element | undefined} */
|
|
170
|
+
var node;
|
|
171
|
+
/** @type {string | undefined} */
|
|
172
|
+
var node_ns;
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
if (HYDRATION && hydrating) {
|
|
176
|
+
return /** @type {import('./hydrate.js').HydrationRuntime} */ (H).t(false, 1);
|
|
177
|
+
}
|
|
178
|
+
var ns = active_namespace;
|
|
179
|
+
if (node === undefined || node_ns !== ns) {
|
|
180
|
+
var doc = (inert_document ??= document.createElement('template').content.ownerDocument);
|
|
181
|
+
node =
|
|
182
|
+
ns === DEFAULT_NAMESPACE
|
|
183
|
+
? doc.createElement(tag)
|
|
184
|
+
: doc.createElementNS(NAMESPACE_URI[ns], tag);
|
|
185
|
+
if (attributes !== null) {
|
|
186
|
+
for (var i = 0; i < attributes.length; i += 2) {
|
|
187
|
+
node.setAttribute(attributes[i], attributes[i + 1]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (text !== '') {
|
|
191
|
+
node.textContent = text;
|
|
192
|
+
}
|
|
193
|
+
node_ns = ns;
|
|
194
|
+
}
|
|
195
|
+
var clone = is_firefox ? document.importNode(node, true) : node.cloneNode(true);
|
|
196
|
+
assign_nodes(clone, clone);
|
|
197
|
+
return clone;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
145
201
|
/**
|
|
146
202
|
* Appends a DOM node before the anchor node.
|
|
147
203
|
* @param {ChildNode | AppendIntoAnchor} anchor - The anchor node.
|
|
@@ -76,6 +76,46 @@ describe('composite > dynamic components', () => {
|
|
|
76
76
|
expect(container.textContent).toBe('hidden');
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
it(
|
|
80
|
+
'remounts a childless dynamic component with fresh props when its tag expression re-evaluates',
|
|
81
|
+
() => {
|
|
82
|
+
let bump: () => void = () => {};
|
|
83
|
+
let poke: () => void = () => {};
|
|
84
|
+
|
|
85
|
+
function Child(props: { n: number }) @{
|
|
86
|
+
<span>{props.n}</span>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function App() @{
|
|
90
|
+
const n = track(0);
|
|
91
|
+
const flag = track(true);
|
|
92
|
+
bump = () => {
|
|
93
|
+
n.value++;
|
|
94
|
+
};
|
|
95
|
+
poke = () => {
|
|
96
|
+
flag.value = !flag.value;
|
|
97
|
+
};
|
|
98
|
+
// The tag expression reads `flag`, but always resolves to `Child`.
|
|
99
|
+
<{flag.value || !flag.value ? Child : null} n={n.value} />
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
render(App);
|
|
103
|
+
flushSync();
|
|
104
|
+
const span = () => container.querySelector('span');
|
|
105
|
+
expect(span()?.textContent).toBe('0');
|
|
106
|
+
|
|
107
|
+
// A prop is a value: the instance keeps the number it got.
|
|
108
|
+
bump();
|
|
109
|
+
flushSync();
|
|
110
|
+
expect(span()?.textContent).toBe('0');
|
|
111
|
+
|
|
112
|
+
// The tag expression re-evaluated: a new instance reads the literal again.
|
|
113
|
+
poke();
|
|
114
|
+
flushSync();
|
|
115
|
+
expect(span()?.textContent).toBe('1');
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
79
119
|
it('handles dynamic component switching', () => {
|
|
80
120
|
function Child1() @{
|
|
81
121
|
<div>{'I am child 1'}</div>
|
|
@@ -645,8 +645,10 @@ export function Precedence() @{
|
|
|
645
645
|
// block is written after the nested scope.
|
|
646
646
|
expect(black).toBeLessThan(italic);
|
|
647
647
|
expect(italic).toBeLessThan(blue);
|
|
648
|
+
// The outer div shares a fragment template with the placeholder that
|
|
649
|
+
// follows it; the inner div is a flat element template of its own.
|
|
648
650
|
expect(code).toContain(`class="outer ${outer} ${theme_hash}"`);
|
|
649
|
-
expect(code).toContain(`class
|
|
651
|
+
expect(code).toContain(`'class', 'inner ${outer} ${inner} ${theme_hash}'`);
|
|
650
652
|
},
|
|
651
653
|
);
|
|
652
654
|
});
|
|
@@ -1,7 +1,279 @@
|
|
|
1
|
-
import { track } from 'ripple';
|
|
1
|
+
import { track, flushSync } from 'ripple';
|
|
2
2
|
import type { Component, PropsWithChildren, PropsWithExtras } from 'ripple';
|
|
3
3
|
|
|
4
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
5
|
+
const HTML_NS = 'http://www.w3.org/1999/xhtml';
|
|
6
|
+
|
|
7
|
+
// The namespace is only known at runtime here: the component's children are
|
|
8
|
+
// compiled without it and rendered into an `<svg>` the component owns, so
|
|
9
|
+
// every block must keep the namespace it was created in when it reruns.
|
|
10
|
+
describe('SVG namespace in runtime-only contexts', () => {
|
|
11
|
+
function SVG({ children }: PropsWithChildren<{}>) @{
|
|
12
|
+
<svg>{children}</svg>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
it('parses a nested template in the SVG namespace', () => {
|
|
16
|
+
function App() @{
|
|
17
|
+
<SVG>
|
|
18
|
+
<g class="group">
|
|
19
|
+
<circle r="1" />
|
|
20
|
+
</g>
|
|
21
|
+
</SVG>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
render(App);
|
|
25
|
+
expect(container.querySelector('.group').namespaceURI).toBe(SVG_NS);
|
|
26
|
+
expect(container.querySelector('circle').namespaceURI).toBe(SVG_NS);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('keeps the namespace when an @if in component children toggles', () => {
|
|
30
|
+
let setFlag: ((value: boolean) => void) | undefined;
|
|
31
|
+
|
|
32
|
+
function App() @{
|
|
33
|
+
const flag = track(true);
|
|
34
|
+
setFlag = (value) => {
|
|
35
|
+
flag.value = value;
|
|
36
|
+
};
|
|
37
|
+
<SVG>
|
|
38
|
+
@if (flag.value) {
|
|
39
|
+
<polygon class="a" points="0,0 1,1" />
|
|
40
|
+
} @else {
|
|
41
|
+
<g class="b">
|
|
42
|
+
<rect width="1" height="1" />
|
|
43
|
+
</g>
|
|
44
|
+
}
|
|
45
|
+
</SVG>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
render(App);
|
|
49
|
+
expect(container.querySelector('.a').namespaceURI).toBe(SVG_NS);
|
|
50
|
+
flushSync(() => setFlag?.(false));
|
|
51
|
+
expect(container.querySelector('.b').namespaceURI).toBe(SVG_NS);
|
|
52
|
+
expect(container.querySelector('rect').namespaceURI).toBe(SVG_NS);
|
|
53
|
+
flushSync(() => setFlag?.(true));
|
|
54
|
+
expect(container.querySelector('.a').namespaceURI).toBe(SVG_NS);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('keeps the namespace when an @if in a leaf component toggles', () => {
|
|
58
|
+
let setFlag: ((value: boolean) => void) | undefined;
|
|
59
|
+
|
|
60
|
+
function Shape() @{
|
|
61
|
+
const flag = track(true);
|
|
62
|
+
setFlag = (value) => {
|
|
63
|
+
flag.value = value;
|
|
64
|
+
};
|
|
65
|
+
@if (flag.value) {
|
|
66
|
+
<polygon class="a" points="0,0 1,1" />
|
|
67
|
+
} @else {
|
|
68
|
+
<rect class="b" width="1" height="1" />
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function App() @{
|
|
73
|
+
<svg>
|
|
74
|
+
<Shape />
|
|
75
|
+
</svg>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
render(App);
|
|
79
|
+
expect(container.querySelector('.a').namespaceURI).toBe(SVG_NS);
|
|
80
|
+
flushSync(() => setFlag?.(false));
|
|
81
|
+
expect(container.querySelector('.b').namespaceURI).toBe(SVG_NS);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('keeps the namespace when a dynamic tag in component children changes', () => {
|
|
85
|
+
let setTag: ((value: string) => void) | undefined;
|
|
86
|
+
|
|
87
|
+
function App() @{
|
|
88
|
+
const tag = track('polygon');
|
|
89
|
+
setTag = (value) => {
|
|
90
|
+
tag.value = value;
|
|
91
|
+
};
|
|
92
|
+
<SVG>
|
|
93
|
+
<{tag.value} class="shape" />
|
|
94
|
+
</SVG>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render(App);
|
|
98
|
+
expect(container.querySelector('.shape').namespaceURI).toBe(SVG_NS);
|
|
99
|
+
flushSync(() => setTag?.('rect'));
|
|
100
|
+
const shape = container.querySelector('.shape');
|
|
101
|
+
expect(shape.localName).toBe('rect');
|
|
102
|
+
expect(shape.namespaceURI).toBe(SVG_NS);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('keeps the namespace for items added to an @for in component children', () => {
|
|
106
|
+
let setItems: ((value: string[]) => void) | undefined;
|
|
107
|
+
|
|
108
|
+
function App() @{
|
|
109
|
+
const items = track(['a']);
|
|
110
|
+
setItems = (value) => {
|
|
111
|
+
items.value = value;
|
|
112
|
+
};
|
|
113
|
+
<SVG>
|
|
114
|
+
@for (const id of items.value) {
|
|
115
|
+
<circle data-id={id} r="1" />
|
|
116
|
+
}
|
|
117
|
+
</SVG>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
render(App);
|
|
121
|
+
expect(container.querySelector('[data-id=a]').namespaceURI).toBe(SVG_NS);
|
|
122
|
+
flushSync(() => setItems?.(['a', 'b']));
|
|
123
|
+
expect(container.querySelector('[data-id=b]').namespaceURI).toBe(SVG_NS);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('renders HTML inside a foreignObject in component children', () => {
|
|
127
|
+
let setTag: ((value: string) => void) | undefined;
|
|
128
|
+
|
|
129
|
+
function App() @{
|
|
130
|
+
const tag = track('div');
|
|
131
|
+
setTag = (value) => {
|
|
132
|
+
tag.value = value;
|
|
133
|
+
};
|
|
134
|
+
<SVG>
|
|
135
|
+
<foreignObject>
|
|
136
|
+
@if (tag.value !== 'none') {
|
|
137
|
+
<div class="static-label">
|
|
138
|
+
<span>hi</span>
|
|
139
|
+
</div>
|
|
140
|
+
}
|
|
141
|
+
<{tag.value} class="html-label">Label</{tag.value}>
|
|
142
|
+
</foreignObject>
|
|
143
|
+
<circle class="after" r="1" />
|
|
144
|
+
</SVG>
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
render(App);
|
|
148
|
+
expect(container.querySelector('.static-label').namespaceURI).toBe(HTML_NS);
|
|
149
|
+
expect(container.querySelector('span').namespaceURI).toBe(HTML_NS);
|
|
150
|
+
expect(container.querySelector('.html-label').namespaceURI).toBe(HTML_NS);
|
|
151
|
+
expect(container.querySelector('.after').namespaceURI).toBe(SVG_NS);
|
|
152
|
+
flushSync(() => setTag?.('span'));
|
|
153
|
+
const label = container.querySelector('.html-label');
|
|
154
|
+
expect(label.localName).toBe('span');
|
|
155
|
+
expect(label.namespaceURI).toBe(HTML_NS);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('sets a dynamic class on SVG elements in component children', () => {
|
|
159
|
+
let setName: ((value: string) => void) | undefined;
|
|
160
|
+
|
|
161
|
+
function App() @{
|
|
162
|
+
const name = track('one');
|
|
163
|
+
setName = (value) => {
|
|
164
|
+
name.value = value;
|
|
165
|
+
};
|
|
166
|
+
<SVG>
|
|
167
|
+
<circle class={name.value} r="1" />
|
|
168
|
+
<a class={name.value} href="#link">
|
|
169
|
+
<text>link</text>
|
|
170
|
+
</a>
|
|
171
|
+
</SVG>
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
render(App);
|
|
175
|
+
expect(container.querySelector('circle').getAttribute('class')).toBe('one');
|
|
176
|
+
expect(container.querySelector('a').getAttribute('class')).toBe('one');
|
|
177
|
+
flushSync(() => setName?.('two'));
|
|
178
|
+
expect(container.querySelector('circle').getAttribute('class')).toBe('two');
|
|
179
|
+
expect(container.querySelector('a').getAttribute('class')).toBe('two');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('dynamic element namespace changes', () => {
|
|
184
|
+
it('switches a root between HTML and SVG with its children', () => {
|
|
185
|
+
let setTag: ((value: string) => void) | undefined;
|
|
186
|
+
|
|
187
|
+
function App() @{
|
|
188
|
+
const tag = track('div');
|
|
189
|
+
setTag = (value) => {
|
|
190
|
+
tag.value = value;
|
|
191
|
+
};
|
|
192
|
+
<{tag.value} class="root">
|
|
193
|
+
<g class="inner">
|
|
194
|
+
<title>t</title>
|
|
195
|
+
</g>
|
|
196
|
+
</{tag.value}>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
render(App);
|
|
200
|
+
expect(container.querySelector('.root').namespaceURI).toBe(HTML_NS);
|
|
201
|
+
expect(container.querySelector('.inner').namespaceURI).toBe(HTML_NS);
|
|
202
|
+
flushSync(() => setTag?.('svg'));
|
|
203
|
+
expect(container.querySelector('.root').namespaceURI).toBe(SVG_NS);
|
|
204
|
+
expect(container.querySelector('.inner').namespaceURI).toBe(SVG_NS);
|
|
205
|
+
expect(container.querySelector('title').namespaceURI).toBe(SVG_NS);
|
|
206
|
+
flushSync(() => setTag?.('div'));
|
|
207
|
+
expect(container.querySelector('.root').namespaceURI).toBe(HTML_NS);
|
|
208
|
+
expect(container.querySelector('.inner').namespaceURI).toBe(HTML_NS);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('renders HTML children once the tag becomes foreignObject', () => {
|
|
212
|
+
let setTag: ((value: string) => void) | undefined;
|
|
213
|
+
|
|
214
|
+
function App() @{
|
|
215
|
+
const tag = track('g');
|
|
216
|
+
setTag = (value) => {
|
|
217
|
+
tag.value = value;
|
|
218
|
+
};
|
|
219
|
+
<svg>
|
|
220
|
+
<{tag.value} class="host">
|
|
221
|
+
<a class="content">hi</a>
|
|
222
|
+
</{tag.value}>
|
|
223
|
+
</svg>
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
render(App);
|
|
227
|
+
expect(container.querySelector('.host').namespaceURI).toBe(SVG_NS);
|
|
228
|
+
expect(container.querySelector('.content').namespaceURI).toBe(SVG_NS);
|
|
229
|
+
flushSync(() => setTag?.('foreignObject'));
|
|
230
|
+
const host = container.querySelector('.host');
|
|
231
|
+
expect(host.localName).toBe('foreignObject');
|
|
232
|
+
expect(host.namespaceURI).toBe(SVG_NS);
|
|
233
|
+
expect(container.querySelector('.content').namespaceURI).toBe(HTML_NS);
|
|
234
|
+
flushSync(() => setTag?.('g'));
|
|
235
|
+
expect(container.querySelector('.content').namespaceURI).toBe(SVG_NS);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
4
239
|
describe('SVG namespace handling', () => {
|
|
240
|
+
it('preserves SVG and foreignObject namespaces when dynamic tags change', () => {
|
|
241
|
+
let setTags: ((svgTag: string, htmlTag: string) => void) | undefined;
|
|
242
|
+
|
|
243
|
+
function App() @{
|
|
244
|
+
const shape = track('circle');
|
|
245
|
+
const html = track('div');
|
|
246
|
+
setTags = (svgTag, htmlTag) => {
|
|
247
|
+
shape.value = svgTag;
|
|
248
|
+
html.value = htmlTag;
|
|
249
|
+
};
|
|
250
|
+
<svg>
|
|
251
|
+
<{shape.value} class="shape">
|
|
252
|
+
<title>Dynamic shape</title>
|
|
253
|
+
</{shape.value}>
|
|
254
|
+
<foreignObject>
|
|
255
|
+
<{html.value} class="html-label">Label</{html.value}>
|
|
256
|
+
</foreignObject>
|
|
257
|
+
</svg>
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
render(App);
|
|
261
|
+
for (const [svgTag, htmlTag] of [['circle', 'div'], ['path', 'span'], ['line', 'div']]) {
|
|
262
|
+
flushSync(() => {
|
|
263
|
+
setTags?.(svgTag, htmlTag);
|
|
264
|
+
});
|
|
265
|
+
const element = container.querySelector('.shape');
|
|
266
|
+
expect(element.localName).toBe(svgTag);
|
|
267
|
+
expect(element.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
268
|
+
expect(element.querySelector('title').namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
269
|
+
expect(element.textContent).toBe('Dynamic shape');
|
|
270
|
+
const label = container.querySelector('.html-label');
|
|
271
|
+
expect(label.localName).toBe(htmlTag);
|
|
272
|
+
expect(label.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
|
|
273
|
+
expect(label.textContent).toBe('Label');
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
5
277
|
it('should render static SVG elements with correct namespace', () => {
|
|
6
278
|
function App() @{
|
|
7
279
|
<svg
|