Skip to content

Commit 4f06eeb

Browse files
committed
fix(dom): fix <svg> and <foreignObject> mount and updates
1 parent da8c31d commit 4f06eeb

File tree

3 files changed

+85
-13
lines changed

3 files changed

+85
-13
lines changed

packages/compiler-core/src/transforms/transformElement.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,7 @@ export const transformElement: NodeTransform = (node, context) => {
7070
let runtimeDirectives: DirectiveNode[] | undefined
7171
let dynamicPropNames: string[] | undefined
7272
let dynamicComponent: string | CallExpression | undefined
73-
// technically this is web specific but we are keeping it in core to avoid
74-
// extra complexity
75-
let isSVG = false
73+
let shouldUseBlock = false
7674

7775
// handle dynamic component
7876
const isProp = findProp(node, 'is')
@@ -110,8 +108,12 @@ export const transformElement: NodeTransform = (node, context) => {
110108
nodeType = toValidAssetId(tag, `component`)
111109
} else {
112110
// plain element
113-
nodeType = `"${node.tag}"`
114-
isSVG = node.tag === 'svg'
111+
nodeType = `"${tag}"`
112+
// <svg> and <foreignObject> must be forced into blocks so that block
113+
// updates inside get proper isSVG flag at runtime. (#639, #643)
114+
// This is technically web-specific, but splitting the logic out of core
115+
// leads to too much unnecessary complexity.
116+
shouldUseBlock = tag === 'svg' || tag === 'foreignObject'
115117
}
116118

117119
const args: CallExpression['arguments'] = [nodeType]
@@ -197,10 +199,8 @@ export const transformElement: NodeTransform = (node, context) => {
197199
}
198200

199201
const { loc } = node
200-
const vnode = isSVG
201-
? // <svg> must be forced into blocks so that block updates inside retain
202-
// isSVG flag at runtime. (#639, #643)
203-
createSequenceExpression([
202+
const vnode = shouldUseBlock
203+
? createSequenceExpression([
204204
createCallExpression(context.helper(OPEN_BLOCK)),
205205
createCallExpression(context.helper(CREATE_BLOCK), args, loc)
206206
])

packages/runtime-core/src/renderer.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ export function createRenderer<
370370
optimized: boolean
371371
) {
372372
const el = (vnode.el = hostCreateElement(vnode.type as string, isSVG))
373-
const { props, shapeFlag, transition, scopeId } = vnode
373+
const { type, props, shapeFlag, transition, scopeId } = vnode
374374

375375
// props
376376
if (props != null) {
@@ -406,7 +406,7 @@ export function createRenderer<
406406
null,
407407
parentComponent,
408408
parentSuspense,
409-
isSVG,
409+
isSVG && type !== 'foreignObject',
410410
optimized || vnode.dynamicChildren !== null
411411
)
412412
}
@@ -562,18 +562,27 @@ export function createRenderer<
562562
)
563563
}
564564

565+
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
565566
if (dynamicChildren != null) {
566567
patchBlockChildren(
567568
n1.dynamicChildren!,
568569
dynamicChildren,
569570
el,
570571
parentComponent,
571572
parentSuspense,
572-
isSVG
573+
areChildrenSVG
573574
)
574575
} else if (!optimized) {
575576
// full diff
576-
patchChildren(n1, n2, el, null, parentComponent, parentSuspense, isSVG)
577+
patchChildren(
578+
n1,
579+
n2,
580+
el,
581+
null,
582+
parentComponent,
583+
parentSuspense,
584+
areChildrenSVG
585+
)
577586
}
578587

579588
if (newProps.onVnodeUpdated != null) {

packages/vue/__tests__/svg.spec.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// SVG logic is technically dom-specific, but the logic is placed in core
2+
// because splitting it out of core would lead to unnecessary complexity in both
3+
// the renderer and compiler implementations.
4+
// Related files:
5+
// - runtime-core/src/renderer.ts
6+
// - compiler-core/src/transoforms/transformElement.ts
7+
8+
import { render, h, ref, nextTick } from '../src'
9+
10+
describe('SVG support', () => {
11+
test('should mount elements with correct namespaces', () => {
12+
const root = document.createElement('div')
13+
document.body.appendChild(root)
14+
const App = {
15+
template: `
16+
<div id="e0">
17+
<svg id="e1">
18+
<foreignObject id="e2">
19+
<div id="e3"/>
20+
</foreignObject>
21+
</svg>
22+
</div>
23+
`
24+
}
25+
render(h(App), root)
26+
const e0 = document.getElementById('e0')!
27+
expect(e0.namespaceURI).toMatch('xhtml')
28+
expect(e0.querySelector('#e1')!.namespaceURI).toMatch('svg')
29+
expect(e0.querySelector('#e2')!.namespaceURI).toMatch('svg')
30+
expect(e0.querySelector('#e3')!.namespaceURI).toMatch('xhtml')
31+
})
32+
33+
test('should patch elements with correct namespaces', async () => {
34+
const root = document.createElement('div')
35+
document.body.appendChild(root)
36+
const cls = ref('foo')
37+
const App = {
38+
setup: () => ({ cls }),
39+
template: `
40+
<div>
41+
<svg id="f1" :class="cls">
42+
<foreignObject>
43+
<div id="f2" :class="cls"/>
44+
</foreignObject>
45+
</svg>
46+
</div>
47+
`
48+
}
49+
render(h(App), root)
50+
const f1 = document.querySelector('#f1')!
51+
const f2 = document.querySelector('#f2')!
52+
expect(f1.getAttribute('class')).toBe('foo')
53+
expect(f2.className).toBe('foo')
54+
55+
// set a transition class on the <div> - which is only respected on non-svg
56+
// patches
57+
;(f2 as any)._vtc = ['baz']
58+
cls.value = 'bar'
59+
await nextTick()
60+
expect(f1.getAttribute('class')).toBe('bar')
61+
expect(f2.className).toBe('bar baz')
62+
})
63+
})

0 commit comments

Comments
 (0)