diff --git a/packages/runtime-core/__tests__/componentPublicInstance.spec.ts b/packages/runtime-core/__tests__/componentPublicInstance.spec.ts index c9958efc7a1..d6e3028d36e 100644 --- a/packages/runtime-core/__tests__/componentPublicInstance.spec.ts +++ b/packages/runtime-core/__tests__/componentPublicInstance.spec.ts @@ -1,9 +1,14 @@ import { + Fragment, + TestNodeTypes, createApp, + createCommentVNode, + createElementBlock, defineComponent, getCurrentInstance, h, nodeOps, + openBlock, render, shallowReadonly, } from '@vue/runtime-test' @@ -11,6 +16,7 @@ import type { ComponentInternalInstance, ComponentOptions, } from '../src/component' +import { PatchFlags } from '@vue/shared' describe('component: proxy', () => { test('data', () => { @@ -107,6 +113,30 @@ describe('component: proxy', () => { expect(nextTickThis).toBe(instanceProxy) }) + // #12680 + test('$el should resolve to the real root element when root is a dev comment + single element fragment', () => { + let instanceProxy: any + const Comp = { + setup() { + return () => ( + openBlock(), + createElementBlock( + Fragment, + null, + [createCommentVNode(' comment '), h('div', 'real root')], + PatchFlags.DEV_ROOT_FRAGMENT, + ) + ) + }, + mounted() { + instanceProxy = this + }, + } + render(h(Comp), nodeOps.createElement('div')) + expect(instanceProxy.$el.type).toBe(TestNodeTypes.ELEMENT) + expect(instanceProxy.$el.tag).toBe('div') + }) + test('user attached properties', async () => { let instance: ComponentInternalInstance let instanceProxy: any diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 7a8af587167..fc0a73f76ad 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -17,6 +17,7 @@ import { EMPTY_OBJ, type IfAny, NOOP, + PatchFlags, type Prettify, type UnionToIntersection, extend, @@ -51,11 +52,12 @@ import { } from './componentOptions' import type { EmitFn, EmitsOptions } from './componentEmits' import type { SlotsType, UnwrapSlotsType } from './componentSlots' -import { markAttrsAccessed } from './componentRenderUtils' +import { filterSingleRoot, markAttrsAccessed } from './componentRenderUtils' import { currentRenderingInstance } from './componentRenderContext' import { warn } from './warning' import { installCompatInstanceProperties } from './compat/instance' import type { Directive } from './directives' +import { Fragment, type VNodeArrayChildren } from './vnode' /** * Custom properties added to component instances in any way and can be accessed through `this` @@ -358,12 +360,33 @@ const getPublicInstance = ( return getPublicInstance(i.parent) } +// dev only: in dev mode, a leading comment before a component's single root +// element is preserved and turns the root into a `DEV_ROOT_FRAGMENT`, whose +// `vnode.el` is the fragment's anchor rather than the actual rendered +// element. Resolve through to the real root here, mirroring the resolution +// already done for attrs/scopeId fallthrough in `renderComponentRoot`. +const getDevRootFragmentEl = (i: ComponentInternalInstance) => { + const { subTree } = i + if ( + subTree && + subTree.type === Fragment && + subTree.patchFlag > 0 && + subTree.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT + ) { + const realRoot = filterSingleRoot(subTree.children as VNodeArrayChildren) + if (realRoot) { + return realRoot.el + } + } + return i.vnode.el +} + export const publicPropertiesMap: PublicPropertiesMap = // Move PURE marker to new line to workaround compiler discarding it // due to type annotation /*@__PURE__*/ extend(Object.create(null), { $: i => i, - $el: i => i.vnode.el, + $el: i => (__DEV__ ? getDevRootFragmentEl(i) : i.vnode.el), $data: i => i.data, $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props), $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),