fix(dom): fix <svg> and <foreignObject> mount and updates

This commit is contained in:
Evan You 2020-01-21 11:32:17 -05:00
parent da8c31dc7f
commit 4f06eebc1c
3 changed files with 85 additions and 13 deletions

View File

@ -70,9 +70,7 @@ export const transformElement: NodeTransform = (node, context) => {
let runtimeDirectives: DirectiveNode[] | undefined let runtimeDirectives: DirectiveNode[] | undefined
let dynamicPropNames: string[] | undefined let dynamicPropNames: string[] | undefined
let dynamicComponent: string | CallExpression | undefined let dynamicComponent: string | CallExpression | undefined
// technically this is web specific but we are keeping it in core to avoid let shouldUseBlock = false
// extra complexity
let isSVG = false
// handle dynamic component // handle dynamic component
const isProp = findProp(node, 'is') const isProp = findProp(node, 'is')
@ -110,8 +108,12 @@ export const transformElement: NodeTransform = (node, context) => {
nodeType = toValidAssetId(tag, `component`) nodeType = toValidAssetId(tag, `component`)
} else { } else {
// plain element // plain element
nodeType = `"${node.tag}"` nodeType = `"${tag}"`
isSVG = node.tag === 'svg' // <svg> and <foreignObject> must be forced into blocks so that block
// updates inside get proper isSVG flag at runtime. (#639, #643)
// This is technically web-specific, but splitting the logic out of core
// leads to too much unnecessary complexity.
shouldUseBlock = tag === 'svg' || tag === 'foreignObject'
} }
const args: CallExpression['arguments'] = [nodeType] const args: CallExpression['arguments'] = [nodeType]
@ -197,10 +199,8 @@ export const transformElement: NodeTransform = (node, context) => {
} }
const { loc } = node const { loc } = node
const vnode = isSVG const vnode = shouldUseBlock
? // <svg> must be forced into blocks so that block updates inside retain ? createSequenceExpression([
// isSVG flag at runtime. (#639, #643)
createSequenceExpression([
createCallExpression(context.helper(OPEN_BLOCK)), createCallExpression(context.helper(OPEN_BLOCK)),
createCallExpression(context.helper(CREATE_BLOCK), args, loc) createCallExpression(context.helper(CREATE_BLOCK), args, loc)
]) ])

View File

@ -370,7 +370,7 @@ export function createRenderer<
optimized: boolean optimized: boolean
) { ) {
const el = (vnode.el = hostCreateElement(vnode.type as string, isSVG)) const el = (vnode.el = hostCreateElement(vnode.type as string, isSVG))
const { props, shapeFlag, transition, scopeId } = vnode const { type, props, shapeFlag, transition, scopeId } = vnode
// props // props
if (props != null) { if (props != null) {
@ -406,7 +406,7 @@ export function createRenderer<
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, isSVG && type !== 'foreignObject',
optimized || vnode.dynamicChildren !== null optimized || vnode.dynamicChildren !== null
) )
} }
@ -562,6 +562,7 @@ export function createRenderer<
) )
} }
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
if (dynamicChildren != null) { if (dynamicChildren != null) {
patchBlockChildren( patchBlockChildren(
n1.dynamicChildren!, n1.dynamicChildren!,
@ -569,11 +570,19 @@ export function createRenderer<
el, el,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG areChildrenSVG
) )
} else if (!optimized) { } else if (!optimized) {
// full diff // full diff
patchChildren(n1, n2, el, null, parentComponent, parentSuspense, isSVG) patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG
)
} }
if (newProps.onVnodeUpdated != null) { if (newProps.onVnodeUpdated != null) {

View File

@ -0,0 +1,63 @@
// SVG logic is technically dom-specific, but the logic is placed in core
// because splitting it out of core would lead to unnecessary complexity in both
// the renderer and compiler implementations.
// Related files:
// - runtime-core/src/renderer.ts
// - compiler-core/src/transoforms/transformElement.ts
import { render, h, ref, nextTick } from '../src'
describe('SVG support', () => {
test('should mount elements with correct namespaces', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const App = {
template: `
<div id="e0">
<svg id="e1">
<foreignObject id="e2">
<div id="e3"/>
</foreignObject>
</svg>
</div>
`
}
render(h(App), root)
const e0 = document.getElementById('e0')!
expect(e0.namespaceURI).toMatch('xhtml')
expect(e0.querySelector('#e1')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e2')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e3')!.namespaceURI).toMatch('xhtml')
})
test('should patch elements with correct namespaces', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const cls = ref('foo')
const App = {
setup: () => ({ cls }),
template: `
<div>
<svg id="f1" :class="cls">
<foreignObject>
<div id="f2" :class="cls"/>
</foreignObject>
</svg>
</div>
`
}
render(h(App), root)
const f1 = document.querySelector('#f1')!
const f2 = document.querySelector('#f2')!
expect(f1.getAttribute('class')).toBe('foo')
expect(f2.className).toBe('foo')
// set a transition class on the <div> - which is only respected on non-svg
// patches
;(f2 as any)._vtc = ['baz']
cls.value = 'bar'
await nextTick()
expect(f1.getAttribute('class')).toBe('bar')
expect(f2.className).toBe('bar baz')
})
})