Skip to content

Commit ea95475

Browse files
committed
support dynamic svelte:element namespace through xmlns attribute
1 parent e304445 commit ea95475

File tree

5 files changed

+52
-21
lines changed

5 files changed

+52
-21
lines changed

.changeset/hip-pumpkins-boil.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: fall back to component namespace when not statically determinable, add way to tell `<svelte:element>` the namespace at runtime

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2008,6 +2008,9 @@ export const template_visitors = {
20082008
/** @type {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} */
20092009
const attributes = [];
20102010

2011+
/** @type {import('#compiler').Attribute['value'] | undefined} */
2012+
let dynamic_namespace = undefined;
2013+
20112014
/** @type {import('#compiler').ClassDirective[]} */
20122015
const class_directives = [];
20132016

@@ -2036,7 +2039,11 @@ export const template_visitors = {
20362039

20372040
for (const attribute of node.attributes) {
20382041
if (attribute.type === 'Attribute') {
2039-
attributes.push(attribute);
2042+
if (attribute.name === 'xmlns' && !is_text_attribute(attribute)) {
2043+
dynamic_namespace = attribute.value;
2044+
} else {
2045+
attributes.push(attribute);
2046+
}
20402047
} else if (attribute.type === 'SpreadAttribute') {
20412048
attributes.push(attribute);
20422049
} else if (attribute.type === 'ClassDirective') {
@@ -2090,19 +2097,16 @@ export const template_visitors = {
20902097
}
20912098
})
20922099
);
2093-
context.state.init.push(
2094-
b.stmt(
2095-
b.call(
2096-
'$.element',
2097-
context.state.node,
2098-
get_tag,
2099-
node.metadata.svg ? b.true : b.false,
2100-
inner.length === 0
2101-
? /** @type {any} */ (undefined)
2102-
: b.arrow([element_id, b.id('$$anchor')], b.block(inner))
2103-
)
2104-
)
2105-
);
2100+
2101+
const args = [context.state.node, get_tag, node.metadata.svg ? b.true : b.false];
2102+
if (inner.length > 0) {
2103+
args.push(b.arrow([element_id, b.id('$$anchor')], b.block(inner)));
2104+
}
2105+
if (dynamic_namespace) {
2106+
if (inner.length === 0) args.push(b.id('undefined'));
2107+
args.push(b.thunk(serialize_attribute_value(dynamic_namespace, context)[1]));
2108+
}
2109+
context.state.init.push(b.stmt(b.call('$.element', ...args)));
21062110
},
21072111
EachBlock(node, context) {
21082112
const each_node_meta = node.metadata;

packages/svelte/src/internal/client/dom/blocks/svelte-element.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ function swap_block_dom(effect, from, to) {
4040
* @param {Comment} anchor
4141
* @param {() => string} get_tag
4242
* @param {boolean} is_svg
43-
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn
43+
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn,
44+
* @param {undefined | (() => string)} get_namespace
4445
* @returns {void}
4546
*/
46-
export function element(anchor, get_tag, is_svg, render_fn) {
47+
export function element(anchor, get_tag, is_svg, render_fn, get_namespace) {
4748
const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
4849

4950
render_effect(() => {
@@ -68,17 +69,14 @@ export function element(anchor, get_tag, is_svg, render_fn) {
6869

6970
block(() => {
7071
const next_tag = get_tag() || null;
72+
const ns = get_namespace?.() || (is_svg || next_tag === 'svg' ? namespace_svg : null);
73+
// Assumption: Noone changes the namespace but not the tag (what would that even mean?)
7174
if (next_tag === tag) return;
7275

7376
// See explanation of `each_item_block` above
7477
var previous_each_item = current_each_item;
7578
set_current_each_item(each_item_block);
7679

77-
// The namespace may not be statically known but we can't really infer it either,
78-
// because on the first render on the client (without hydration) the parent will be undefined,
79-
// and the element itself could be a tag that changes the namespace.
80-
const ns = is_svg || next_tag === 'svg' ? namespace_svg : null;
81-
8280
if (effect) {
8381
if (next_tag === null) {
8482
// start outro
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
async test({ assert, target }) {
5+
assert.equal(target.querySelector('path')?.namespaceURI, 'http://www.w3.org/2000/svg');
6+
7+
await target.querySelector('button')?.click();
8+
assert.equal(target.querySelector('div')?.namespaceURI, 'http://www.w3.org/1999/xhtml');
9+
}
10+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
let tag = $state('path');
3+
let xmlns = $state('http://www.w3.org/2000/svg');
4+
</script>
5+
6+
<button onclick={() => {
7+
tag = 'div';
8+
xmlns = 'http://www.w3.org/1999/xhtml';
9+
}}>change</button>
10+
11+
<!-- wrapper necessary or else jsdom says this is always an xhtml namespace -->
12+
<svg>
13+
<svelte:element this={tag} xmlns={xmlns} />
14+
</svg>

0 commit comments

Comments
 (0)