Skip to content

fix(runtime-core): resolve $el for dev root comment fragment - #15313

Open
oedumoreira wants to merge 3 commits into
vuejs:mainfrom
oedumoreira:fix/12680-el-dev-root-fragment
Open

fix(runtime-core): resolve $el for dev root comment fragment#15313
oedumoreira wants to merge 3 commits into
vuejs:mainfrom
oedumoreira:fix/12680-el-dev-root-fragment

Conversation

@oedumoreira

@oedumoreira oedumoreira commented Aug 18, 2026

Copy link
Copy Markdown

close #12680

In dev mode, Vue preserves HTML comments written in a template. When a component's root is a single element preceded by a leading comment, e.g.:

<!-- a comment -->
<div>real element</div>

...the compiler wraps the root in a fragment flagged DEV_ROOT_FRAGMENT, since there are technically two root-level nodes.

instance.$el (and any template ref pointing at the component) simply read instance.vnode.el, which for this
fragment is the fragment's anchor node — silently resolves to a useless anchor nodeinstead of the real rendered element, in dev only.

Fix

$el now resolves through to the real sing— the same helper renderComponentRootalready uses to resolve attrs/scopeId fallthrough for this exact DEV_ROOT_FRAGMENT case. Only active in __DEV__; production builds strip comments from templates so this scenario doesn't exist there, and the change is a no-op for non-fragment roots.

Test plan

- Added a regression test in packages/runtime-core/__tests__/componentPublicInstance.spec.ts that mounts a
component whose root is [comment, <div>] sserts $el is the real element node (notthe anchor). Verified it fails without the fix and passes with it.
- pnpm test-unit passes locally for runtime-core, runtime-dom, and vue.

close #12680

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

## Bug Fixes

- Fixed component `$el` references in development mode when elements are wrapped in single-root or nested fragments.
- `$el` now correctly resolves to the rendered root element across child components and Suspense boundaries.
- Improved `$el` behavior while child components are mounting.
- Preserved expected fallback behavior for unresolved asynchronous roots.
- Production behavior remains unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

In dev mode, a leading comment before a component's single root
element causes the compiler to wrap the root in a fragment flagged
DEV_ROOT_FRAGMENT. $el (and template refs pointing at the component)
resolved to the fragment's anchor node instead of the real rendered
element, silently breaking any code relying on $el being an actual
DOM element (measuring, focusing, third-party DOM integrations,
etc).

Resolve $el to the real single root via filterSingleRoot, the same
utility already used to resolve attrs/scopeId fallthrough for this
exact scenario in renderComponentRoot. Production builds are
unaffected since dev comments are stripped from templates outside
of dev.

close vuejs#12680
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5bb7db57-1cb9-4033-906a-398dacc2b8e7

📥 Commits

Reviewing files that changed from the base of the PR and between ea51f15 and 03fc92c.

📒 Files selected for processing (2)
  • packages/runtime-core/__tests__/componentPublicInstance.spec.ts
  • packages/runtime-core/src/componentPublicInstance.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Development-mode $el resolution now recursively traverses development fragments, child components, and active Suspense branches. Tests cover direct, nested, mounting, and unresolved async-root cases. Production behavior remains unchanged.

Changes

Development root fragment element resolution

Layer / File(s) Summary
Resolve development root element
packages/runtime-core/src/componentPublicInstance.ts
$el recursively resolves flagged fragments, component subtrees, and active Suspense branches. The resolver preserves the vnode fallback when no element resolves, and production behavior remains unchanged.
Verify component $el
packages/runtime-core/__tests__/componentPublicInstance.spec.ts
Tests cover direct, child, nested, mounting, Suspense, and unresolved async-root cases. Imports and fragment patch flags support the scenarios.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 03fc9

This localized change makes component references resolve to the actual root element in development builds without changing production behavior; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant ComponentInstance
  participant resolveRootEl
  participant RootVNode
  participant RenderedElement
  ComponentInstance->>resolveRootEl: Resolve development root
  resolveRootEl->>RootVNode: Inspect fragment, component, or Suspense subtree
  RootVNode-->>resolveRootEl: Return resolved root vnode
  resolveRootEl->>RenderedElement: Resolve actual element
  RenderedElement-->>ComponentInstance: Set `$el`
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing runtime-core $el resolution for development root comment fragments.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@edison1105 edison1105 added the 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. label Aug 19, 2026

@edison1105 edison1105 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. The overall direction looks correct: DEV_ROOT_FRAGMENT is the right way to distinguish a comment-only pseudo-fragment from a real multi-root component, and resolving this in the public $el getter preserves the renderer's internal fragment-anchor semantics.

However, the current resolver stops at component boundaries. For example:

<!-- Inner.vue -->
<!-- comment -->
<div />
<!-- Outer.vue -->
<Inner />

Outer.subTree is the Inner component VNode, so getDevRootFragmentEl(Outer) falls back to Outer.vnode.el. That value follows the normal component host-element chain to Inner.vnode.el, which is still the DEV_ROOT_FRAGMENT anchor. As a result, Outer.$el remains an anchor instead of the <div>. The same issue remains when both Outer and Inner have root comments: resolving the outer fragment returns the Inner VNode's .el, which is still the inner fragment anchor.

Could we make the public-root resolver recursively treat both DEV_ROOT_FRAGMENT and single-root component VNodes as transparent layers, following vnode.component.subTree where applicable? Please also add a regression test for an outer component rendering an inner component whose root is comment + div; covering the case where both layers are DEV_ROOT_FRAGMENT would be useful as well.

One minor test-fidelity point: the compiler emits PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT, so the hand-written test should preferably use that combination rather than DEV_ROOT_FRAGMENT alone.

The public $el resolver previously only unwrapped a component's own
DEV_ROOT_FRAGMENT, so it stopped at component boundaries: a parent
whose root is a child component (or another DEV_ROOT_FRAGMENT
wrapping one) would still return the fragment anchor instead of the
child's real rendered element. Resolve recursively through both
DEV_ROOT_FRAGMENT layers and single-root component vnodes via
vnode.component.subTree.
@oedumoreira
oedumoreira force-pushed the fix/12680-el-dev-root-fragment branch from ea51f15 to 76149a6 Compare August 20, 2026 14:57
@oedumoreira

Copy link
Copy Markdown
Author

Thanks so much for the detailed review, @edison1105, really appreciate you taking the time to walk through the component-boundary case, it helped me understand the resolution flow much better.

I've updated the PR to address both points:

  • getDevRootFragmentEl now resolves recursively: it unwraps DEV_ROOT_FRAGMENT layers via filterSingleRoot, and also treats single-root component vnodes as transparent by following vnode.component.subTree. This handles the Outer -> Inner case you described, including when both layers are DEV_ROOT_FRAGMENT.
  • Added regression tests for: (1) an outer component rendering an inner component whose root is comment +
    , and (2) both outer and inner having DEV_ROOT_FRAGMENT roots. I also updated the existing test to use PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT to match what the compiler actually emits.

The branch is rebased on the latest main. Let me know if there's anything else you'd like adjusted, I'm still learning the codebase and really enjoying contributing to Vue core, so I'm happy to iterate further if needed.

@oedumoreira
oedumoreira requested a review from edison1105 August 20, 2026 15:10
@edison1105 edison1105 added the ready to merge The PR is ready to be merged. label Aug 21, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@15313
npm i https://pkg.pr.new/@vue/compiler-core@15313
yarn add https://pkg.pr.new/@vue/compiler-core@15313.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@15313
npm i https://pkg.pr.new/@vue/compiler-dom@15313
yarn add https://pkg.pr.new/@vue/compiler-dom@15313.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@15313
npm i https://pkg.pr.new/@vue/compiler-sfc@15313
yarn add https://pkg.pr.new/@vue/compiler-sfc@15313.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@15313
npm i https://pkg.pr.new/@vue/compiler-ssr@15313
yarn add https://pkg.pr.new/@vue/compiler-ssr@15313.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@15313
npm i https://pkg.pr.new/@vue/reactivity@15313
yarn add https://pkg.pr.new/@vue/reactivity@15313.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@15313
npm i https://pkg.pr.new/@vue/runtime-core@15313
yarn add https://pkg.pr.new/@vue/runtime-core@15313.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@15313
npm i https://pkg.pr.new/@vue/runtime-dom@15313
yarn add https://pkg.pr.new/@vue/runtime-dom@15313.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@15313
npm i https://pkg.pr.new/@vue/server-renderer@15313
yarn add https://pkg.pr.new/@vue/server-renderer@15313.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@15313
npm i https://pkg.pr.new/@vue/shared@15313
yarn add https://pkg.pr.new/@vue/shared@15313.tgz

vue

pnpm add https://pkg.pr.new/vue@15313
npm i https://pkg.pr.new/vue@15313
yarn add https://pkg.pr.new/vue@15313.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@15313
npm i https://pkg.pr.new/@vue/compat@15313
yarn add https://pkg.pr.new/@vue/compat@15313.tgz

commit: 03fc92c

@github-actions

Copy link
Copy Markdown

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 108 kB 40.7 kB 36.5 kB
vue.global.prod.js 167 kB 60.9 kB 54.2 kB

Usages

Name Size Gzip Brotli
createApp (CAPI only) 49.7 kB 19.3 kB 17.7 kB
createApp 57.8 kB 22.3 kB 20.4 kB
createSSRApp 62.5 kB 24.3 kB 22.1 kB
defineCustomElement 64.1 kB 24.3 kB 22.1 kB
overall 72.1 kB 27.5 kB 25.1 kB

@edison1105

Copy link
Copy Markdown
Member

/ecosystem-ci run

@vue-bot

vue-bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

📝 Ran ecosystem CI: Open

suite result latest scheduled
quasar success failure
language-tools success success
radix-vue success failure
nuxt success success
vant success success
primevue success failure
vitepress success success
test-utils success success
vue-i18n success success
pinia success success
vite-plugin-vue success success
vuetify success success
vue-macros success success
vueuse success success
router success failure
vue-simple-compiler success failure

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. ready to merge The PR is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

$el of templateRef points to comment VNode instead of first real child of component

3 participants