Skip to content

Commit

Permalink
fix(dom): fix <svg> and <foreignObject> mount and updates
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Jan 21, 2020
1 parent da8c31d commit 4f06eeb
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 13 deletions.
18 changes: 9 additions & 9 deletions packages/compiler-core/src/transforms/transformElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)
])
Expand Down
17 changes: 13 additions & 4 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -406,7 +406,7 @@ export function createRenderer<
null,
parentComponent,
parentSuspense,
isSVG,
isSVG && type !== 'foreignObject',
optimized || vnode.dynamicChildren !== null
)
}
Expand Down Expand Up @@ -562,18 +562,27 @@ export function createRenderer<
)
}

const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
if (dynamicChildren != null) {
patchBlockChildren(
n1.dynamicChildren!,
dynamicChildren,
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) {
Expand Down
63 changes: 63 additions & 0 deletions packages/vue/__tests__/svg.spec.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})

0 comments on commit 4f06eeb

Please sign in to comment.