support dynamic svelte:element namespace through xmlns attribute

pull/11219/head
Simon Holthausen 2 years ago
parent e304445476
commit ea954759c0

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: fall back to component namespace when not statically determinable, add way to tell `<svelte:element>` the namespace at runtime

@ -2008,6 +2008,9 @@ export const template_visitors = {
/** @type {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} */
const attributes = [];
/** @type {import('#compiler').Attribute['value'] | undefined} */
let dynamic_namespace = undefined;
/** @type {import('#compiler').ClassDirective[]} */
const class_directives = [];
@ -2036,7 +2039,11 @@ export const template_visitors = {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
attributes.push(attribute);
if (attribute.name === 'xmlns' && !is_text_attribute(attribute)) {
dynamic_namespace = attribute.value;
} else {
attributes.push(attribute);
}
} else if (attribute.type === 'SpreadAttribute') {
attributes.push(attribute);
} else if (attribute.type === 'ClassDirective') {
@ -2090,19 +2097,16 @@ export const template_visitors = {
}
})
);
context.state.init.push(
b.stmt(
b.call(
'$.element',
context.state.node,
get_tag,
node.metadata.svg ? b.true : b.false,
inner.length === 0
? /** @type {any} */ (undefined)
: b.arrow([element_id, b.id('$$anchor')], b.block(inner))
)
)
);
const args = [context.state.node, get_tag, node.metadata.svg ? b.true : b.false];
if (inner.length > 0) {
args.push(b.arrow([element_id, b.id('$$anchor')], b.block(inner)));
}
if (dynamic_namespace) {
if (inner.length === 0) args.push(b.id('undefined'));
args.push(b.thunk(serialize_attribute_value(dynamic_namespace, context)[1]));
}
context.state.init.push(b.stmt(b.call('$.element', ...args)));
},
EachBlock(node, context) {
const each_node_meta = node.metadata;

@ -40,10 +40,11 @@ function swap_block_dom(effect, from, to) {
* @param {Comment} anchor
* @param {() => string} get_tag
* @param {boolean} is_svg
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn,
* @param {undefined | (() => string)} get_namespace
* @returns {void}
*/
export function element(anchor, get_tag, is_svg, render_fn) {
export function element(anchor, get_tag, is_svg, render_fn, get_namespace) {
const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
render_effect(() => {
@ -68,17 +69,14 @@ export function element(anchor, get_tag, is_svg, render_fn) {
block(() => {
const next_tag = get_tag() || null;
const ns = get_namespace?.() || (is_svg || next_tag === 'svg' ? namespace_svg : null);
// Assumption: Noone changes the namespace but not the tag (what would that even mean?)
if (next_tag === tag) return;
// See explanation of `each_item_block` above
var previous_each_item = current_each_item;
set_current_each_item(each_item_block);
// The namespace may not be statically known but we can't really infer it either,
// because on the first render on the client (without hydration) the parent will be undefined,
// and the element itself could be a tag that changes the namespace.
const ns = is_svg || next_tag === 'svg' ? namespace_svg : null;
if (effect) {
if (next_tag === null) {
// start outro

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
assert.equal(target.querySelector('path')?.namespaceURI, 'http://www.w3.org/2000/svg');
await target.querySelector('button')?.click();
assert.equal(target.querySelector('div')?.namespaceURI, 'http://www.w3.org/1999/xhtml');
}
});

@ -0,0 +1,14 @@
<script>
let tag = $state('path');
let xmlns = $state('http://www.w3.org/2000/svg');
</script>
<button onclick={() => {
tag = 'div';
xmlns = 'http://www.w3.org/1999/xhtml';
}}>change</button>
<!-- wrapper necessary or else jsdom says this is always an xhtml namespace -->
<svg>
<svelte:element this={tag} xmlns={xmlns} />
</svg>
Loading…
Cancel
Save