]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(dom): fix <svg> and <foreignObject> mount and updates
authorEvan You <yyx990803@gmail.com>
Tue, 21 Jan 2020 16:32:17 +0000 (11:32 -0500)
committerEvan You <yyx990803@gmail.com>
Tue, 21 Jan 2020 16:32:33 +0000 (11:32 -0500)
packages/compiler-core/src/transforms/transformElement.ts
packages/runtime-core/src/renderer.ts
packages/vue/__tests__/svg.spec.ts [new file with mode: 0644]

index 09a865112dd18e0727a13455249487730192cca3..0d907498b67ca7f1beb467bc3129e07502db1104 100644 (file)
@@ -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)
         ])
index e136caa4d5e327f1f026b59081c3322ea2dfe1ca..d26f73dae9a0f0cf303f81b6be40af3f306cccce 100644 (file)
@@ -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) {
diff --git a/packages/vue/__tests__/svg.spec.ts b/packages/vue/__tests__/svg.spec.ts
new file mode 100644 (file)
index 0000000..8644d39
--- /dev/null
@@ -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')
+  })
+})