diff --git a/packages/runtime-core/__tests__/apiTemplateRef.spec.ts b/packages/runtime-core/__tests__/apiTemplateRef.spec.ts index 808731c230e..1c82d20ec42 100644 --- a/packages/runtime-core/__tests__/apiTemplateRef.spec.ts +++ b/packages/runtime-core/__tests__/apiTemplateRef.spec.ts @@ -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(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) + }) }) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index a4cf97dc807..670cd41f5c0 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -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 diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 0c468fd06d5..a34358a9ccc 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -64,7 +64,14 @@ export type VNodeRef = | Ref | ((ref: object | null, refs: Record) => 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 + // + // #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( vnode: VNode, - extraProps?: Data & VNodeProps | null + extraProps?: Data & VNodeProps | null, + mergeRef = false ): VNode { // 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( 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 + // 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,