]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-core/refs): handle multiple merged refs for dynamic component with vnode
authorEvan You <yyx990803@gmail.com>
Mon, 14 Sep 2020 19:33:38 +0000 (15:33 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 14 Sep 2020 19:33:38 +0000 (15:33 -0400)
fix #2078

packages/runtime-core/__tests__/apiTemplateRef.spec.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts

index 808731c230e2d14c5e043678e310f67cd024ec1d..1c82d20ec42913da4fe9f103d6cdf610a0ae67e5 100644 (file)
@@ -6,7 +6,8 @@ import {
   nextTick,
   defineComponent,
   reactive,
-  serializeInner
+  serializeInner,
+  shallowRef
 } from '@vue/runtime-test'
 
 // reference: https://vue-composition-api-rfc.netlify.com/api.html#template-refs
@@ -325,4 +326,43 @@ describe('api: template refs', () => {
     await nextTick()
     expect(spy.mock.calls[1][0]).toBe('p')
   })
+
+  // #2078
+  test('handling multiple merged refs', async () => {
+    const Foo = {
+      render: () => h('div', 'foo')
+    }
+    const Bar = {
+      render: () => h('div', 'bar')
+    }
+
+    const viewRef = shallowRef<any>(Foo)
+    const elRef1 = ref()
+    const elRef2 = ref()
+
+    const App = {
+      render() {
+        if (!viewRef.value) {
+          return null
+        }
+        const view = h(viewRef.value, { ref: elRef1 })
+        return h(view, { ref: elRef2 })
+      }
+    }
+    const root = nodeOps.createElement('div')
+    render(h(App), root)
+
+    expect(serializeInner(elRef1.value.$el)).toBe('foo')
+    expect(elRef1.value).toBe(elRef2.value)
+
+    viewRef.value = Bar
+    await nextTick()
+    expect(serializeInner(elRef1.value.$el)).toBe('bar')
+    expect(elRef1.value).toBe(elRef2.value)
+
+    viewRef.value = null
+    await nextTick()
+    expect(elRef1.value).toBeNull()
+    expect(elRef1.value).toBe(elRef2.value)
+  })
 })
index a4cf97dc8077feef5ddb00d943805aa84cdacdaf..670cd41f5c0cf7f0146d468a0dd2a7b9add8d48b 100644 (file)
@@ -10,7 +10,8 @@ import {
   isSameVNodeType,
   Static,
   VNodeNormalizedRef,
-  VNodeHook
+  VNodeHook,
+  VNodeNormalizedRefAtom
 } from './vnode'
 import {
   ComponentInternalInstance,
@@ -284,6 +285,19 @@ export const setRef = (
   parentSuspense: SuspenseBoundary | null,
   vnode: VNode | null
 ) => {
+  if (isArray(rawRef)) {
+    rawRef.forEach((r, i) =>
+      setRef(
+        r,
+        oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),
+        parentComponent,
+        parentSuspense,
+        vnode
+      )
+    )
+    return
+  }
+
   let value: ComponentPublicInstance | RendererNode | null
   if (!vnode) {
     value = null
@@ -295,7 +309,7 @@ export const setRef = (
     }
   }
 
-  const [owner, ref] = rawRef
+  const { i: owner, r: ref } = rawRef
   if (__DEV__ && !owner) {
     warn(
       `Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
@@ -303,7 +317,7 @@ export const setRef = (
     )
     return
   }
-  const oldRef = oldRawRef && oldRawRef[1]
+  const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r
   const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
   const setupState = owner.setupState
 
index 0c468fd06d5b08491403e0a04b6c8c0a22f45729..a34358a9ccc0e8bdf72a893f8d96ba4f47f6bd43 100644 (file)
@@ -64,7 +64,14 @@ export type VNodeRef =
   | Ref
   | ((ref: object | null, refs: Record<string, any>) => void)
 
-export type VNodeNormalizedRef = [ComponentInternalInstance, VNodeRef]
+export type VNodeNormalizedRefAtom = {
+  i: ComponentInternalInstance
+  r: VNodeRef
+}
+
+export type VNodeNormalizedRef =
+  | VNodeNormalizedRefAtom
+  | (VNodeNormalizedRefAtom)[]
 
 type VNodeMountHook = (vnode: VNode) => void
 type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void
@@ -289,11 +296,11 @@ export const InternalObjectKey = `__vInternal`
 const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
   key != null ? key : null
 
-const normalizeRef = ({ ref }: VNodeProps): VNode['ref'] => {
+const normalizeRef = ({ ref }: VNodeProps): VNodeNormalizedRefAtom | null => {
   return (ref != null
     ? isArray(ref)
       ? ref
-      : [currentRenderingInstance!, ref]
+      : { i: currentRenderingInstance, r: ref }
     : null) as any
 }
 
@@ -317,7 +324,10 @@ function _createVNode(
   }
 
   if (isVNode(type)) {
-    const cloned = cloneVNode(type, props)
+    // createVNode receiving an existing vnode. This happens in cases like
+    // <component :is="vnode"/>
+    // #2078 make sure to merge refs during the clone instead of overwriting it
+    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
     if (children) {
       normalizeChildren(cloned, children)
     }
@@ -429,11 +439,12 @@ function _createVNode(
 
 export function cloneVNode<T, U>(
   vnode: VNode<T, U>,
-  extraProps?: Data & VNodeProps | null
+  extraProps?: Data & VNodeProps | null,
+  mergeRef = false
 ): VNode<T, U> {
   // This is intentionally NOT using spread or extend to avoid the runtime
   // key enumeration cost.
-  const { props, patchFlag } = vnode
+  const { props, ref, patchFlag } = vnode
   const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
   return {
     __v_isVNode: true,
@@ -441,7 +452,17 @@ export function cloneVNode<T, U>(
     type: vnode.type,
     props: mergedProps,
     key: mergedProps && normalizeKey(mergedProps),
-    ref: extraProps && extraProps.ref ? normalizeRef(extraProps) : vnode.ref,
+    ref:
+      extraProps && extraProps.ref
+        ? // #2078 in the case of <component :is="vnode" ref="extra"/>
+          // if the vnode itself already has a ref, cloneVNode will need to merge
+          // the refs so the single vnode can be set on multiple refs
+          mergeRef && ref
+          ? isArray(ref)
+            ? ref.concat(normalizeRef(extraProps)!)
+            : [ref, normalizeRef(extraProps)!]
+          : normalizeRef(extraProps)
+        : ref,
     scopeId: vnode.scopeId,
     children: vnode.children,
     target: vnode.target,