fix(dom): fix <svg> and <foreignObject> mount and updates
This commit is contained in:
parent
da8c31dc7f
commit
4f06eebc1c
@ -70,9 +70,7 @@ export const transformElement: NodeTransform = (node, context) => {
|
||||
let runtimeDirectives: DirectiveNode[] | undefined
|
||||
let dynamicPropNames: string[] | undefined
|
||||
let dynamicComponent: string | CallExpression | undefined
|
||||
// technically this is web specific but we are keeping it in core to avoid
|
||||
// extra complexity
|
||||
let isSVG = false
|
||||
let shouldUseBlock = false
|
||||
|
||||
// handle dynamic component
|
||||
const isProp = findProp(node, 'is')
|
||||
@ -110,8 +108,12 @@ export const transformElement: NodeTransform = (node, context) => {
|
||||
nodeType = toValidAssetId(tag, `component`)
|
||||
} else {
|
||||
// plain element
|
||||
nodeType = `"${node.tag}"`
|
||||
isSVG = node.tag === 'svg'
|
||||
nodeType = `"${tag}"`
|
||||
// <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]
|
||||
@ -197,10 +199,8 @@ export const transformElement: NodeTransform = (node, context) => {
|
||||
}
|
||||
|
||||
const { loc } = node
|
||||
const vnode = isSVG
|
||||
? // <svg> must be forced into blocks so that block updates inside retain
|
||||
// isSVG flag at runtime. (#639, #643)
|
||||
createSequenceExpression([
|
||||
const vnode = shouldUseBlock
|
||||
? createSequenceExpression([
|
||||
createCallExpression(context.helper(OPEN_BLOCK)),
|
||||
createCallExpression(context.helper(CREATE_BLOCK), args, loc)
|
||||
])
|
||||
|
@ -370,7 +370,7 @@ export function createRenderer<
|
||||
optimized: boolean
|
||||
) {
|
||||
const el = (vnode.el = hostCreateElement(vnode.type as string, isSVG))
|
||||
const { props, shapeFlag, transition, scopeId } = vnode
|
||||
const { type, props, shapeFlag, transition, scopeId } = vnode
|
||||
|
||||
// props
|
||||
if (props != null) {
|
||||
@ -406,7 +406,7 @@ export function createRenderer<
|
||||
null,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isSVG,
|
||||
isSVG && type !== 'foreignObject',
|
||||
optimized || vnode.dynamicChildren !== null
|
||||
)
|
||||
}
|
||||
@ -562,6 +562,7 @@ export function createRenderer<
|
||||
)
|
||||
}
|
||||
|
||||
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
|
||||
if (dynamicChildren != null) {
|
||||
patchBlockChildren(
|
||||
n1.dynamicChildren!,
|
||||
@ -569,11 +570,19 @@ export function createRenderer<
|
||||
el,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isSVG
|
||||
areChildrenSVG
|
||||
)
|
||||
} else if (!optimized) {
|
||||
// full diff
|
||||
patchChildren(n1, n2, el, null, parentComponent, parentSuspense, isSVG)
|
||||
patchChildren(
|
||||
n1,
|
||||
n2,
|
||||
el,
|
||||
null,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
areChildrenSVG
|
||||
)
|
||||
}
|
||||
|
||||
if (newProps.onVnodeUpdated != null) {
|
||||
|
63
packages/vue/__tests__/svg.spec.ts
Normal file
63
packages/vue/__tests__/svg.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user