8000 fix(custom-element): enhance slot handling with `shadowRoot:false` by edison1105 · Pull Request #13208 · vuejs/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

fix(custom-element): enhance slot handling with shadowRoot:false #13208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

edison1105
Copy link
Member
@edison1105 edison1105 commented Apr 16, 2025

close #13206
close #13234

Note
This PR only fixed bugs in the optimized mode, while issues still exist in the full diff scenario. This happens because _renderSlots only renders slots that have corresponding outlets, meaning not all slots in this._slots are rendered to the DOM tree. During full diff, parent.insertBefore(node, anchor) throws an error because the anchor node isn't actually in the DOM tree.

I couldn't find a suitable solution, so rolled back the full diff related code in 3e72409

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Enhanced support for Vue custom elements without Shadow DOM, improving slot fallback content management and dynamic slot updates.
  • Bug Fixes
    • Fixed element mounting and slot updating issues for custom elements with shadowRoot: false, ensuring correct DOM insertion and slot rendering.
  • Tests
    • Added extensive tests covering slot updates, fallback content, and conditional rendering in custom elements without Shadow DOM for improved stability.

Copy link
github-actions bot commented Apr 16, 2025

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 102 kB (+1.45 kB) 38.6 kB (+412 B) 34.8 kB (+358 B)
vue.global.prod.js 160 kB (+1.45 kB) 58.8 kB (+404 B) 52.3 kB (+378 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 46.6 kB (+74 B) 18.2 kB (+38 B) 16.7 kB (+31 B)
createApp 54.6 kB (+74 B) 21.2 kB (+39 B) 19.4 kB (+41 B)
createSSRApp 58.8 kB (+74 B) 23 kB (+34 B) 21 kB (+40 B)
defineCustomElement 60.7 kB (+1.45 kB) 23.2 kB (+426 B) 21.1 kB (+338 B)
overall 68.6 kB (+74 B) 26.4 kB (+41 B) 24 kB (+41 B)

Copy link
pkg-pr-new bot commented Apr 16, 2025

Open in StackBlitz

@vue/compiler-core

npm i https://pkg.pr.new/@vue/compiler-core@13208

@vue/compiler-dom

npm i https://pkg.pr.new/@vue/compiler-dom@13208

@vue/compiler-sfc

npm i https://pkg.pr.new/@vue/compiler-sfc@13208

@vue/compiler-ssr

npm i https://pkg.pr.new/@vue/compiler-ssr@13208

@vue/runtime-core

npm i https://pkg.pr.new/@vue/runtime-core@13208

@vue/reactivity

npm i https://pkg.pr.new/@vue/reactivity@13208

@vue/runtime-dom

npm i https://pkg.pr.new/@vue/runtime-dom@13208

@vue/server-renderer

npm i https://pkg.pr.new/@vue/server-renderer@13208

@vue/shared

npm i https://pkg.pr.new/@vue/shared@13208

vue

npm i https://pkg.pr.new/vue@13208

@vue/compat

npm i https://pkg.pr.new/@vue/compat@13208

commit: 3e72409

@edison1105 edison1105 marked this pull request as draft April 16, 2025 06:47
@wolandec
Copy link

Hi @edison1105, thanks for this fix, it works well with Vue 3.

However it doesn't work if we create custom elements from Vue 3 components and use them in Vue 2 or React wrapping with Vue 2 or React component similarly as I did in reproduction playground (see CEWrapperOne) . To provide more context: here is a schema of what we are working on at the moment. So we are creating a component library based on Vue 3 but to reuse this implementation in different applications written in Vue 2 or React we use custom elements created from Vue 3 components implementation.

image

@edison1105
Copy link
Member Author

@wolandec
I also found some issues in other scenarios and haven't found a good solution yet, so the PR is still in Draft status.

@wolandec
Copy link

@edison1105 thanks for the clarification, I'll prepare a reproduction repo for our case if it helps.

@wolandec
Copy link
wolandec commented Apr 18, 2025

@edison1105 https://github.com/wolandec/vue-core-issue-13206 Here is the repository with the reproduction of our case of using, I hope it helps. Please let me know if you need any help, thank you!

Copy link
coderabbitai bot commented May 8, 2025

"""

Walkthrough

The changes introduce enhanced support for Vue custom elements using shadowRoot: false, focusing on correct slot and conditional rendering behavior. Core renderer logic, slot parsing, and DOM patching are updated, and new tests are added to verify slot fallback and v-if/v-show handling in custom elements without shadow DOM.

Changes

File(s) Change Summary
packages/runtime-core/src/renderer.ts Modified element mounting and patching logic to handle Vue custom elements with shadowRoot: false, including container reassignment, slot updates via _updateSlots, and fallback handling in patchBlockChildren.
packages/runtime-dom/src/apiCustomElement.ts Enhanced slot management for custom elements without shadow DOM: introduced _slotFallbacks, _slotAnchors, and modified _slots properties; added _updateSlots, _parseSlotFallbacks methods; improved fallback slot capturing and rendering; adjusted slot parsing to retain parent references; added helpers for DOM insertion and node collection; updated cleanup in disconnectedCallback.
packages/runtime-dom/tests/customElement.spec.ts Added tests for custom elements with shadowRoot: false to verify correct slot fallback, conditional rendering (v-if), and slot content switching, both with optimized and standard rendering modes. No changes to existing logic.

Sequence Diagram(s)

sequenceDiagram
    participant Host as Host App
    participant VueCE as Vue Custom Element (shadowRoot: false)
    participant Renderer as Renderer
    participant DOM as DOM

    Host->>VueCE: Insert slotted content (with v-if/v-show)
    VueCE->>Renderer: Mount element
    Renderer->>VueCE: Call _parseSlotFallbacks and _renderSlots
    VueCE->>DOM: Insert anchor and slot/fallback content
    Host->>VueCE: Update reactive state (e.g., v-if toggles)
    VueCE->>Renderer: Patch element
    Renderer->>VueCE: Call _updateSlots (oldVNode, newVNode)
    VueCE->>DOM: Update slot content or fallback as needed
Loading

Assessment against linked issues

Objective Addressed Explanation
Correctly render and update slotted content with v-if/v-show in custom elements ( #13206 )
Prevent errors and ensure named slot content with v-if works without exceptions ( #13234 )

Poem

In the garden of slots, where the shadows don’t grow,
The v-if and v-show now shimmer and glow.
Fallbacks and anchors, a bunny’s delight,
No more null errors in the custom element night!
🐇✨
Hop-hop—slots fixed just right!
"""

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id&g 8000 t;, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@edison1105 edison1105 changed the title fix(custom-element): update slot nodes when shadowRoot is false fix(custom-element): enhance slot handling with shadowRoot:false May 9, 2025
@edison1105 edison1105 added 🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. scope: custom elements ready for review This PR requires more reviews labels May 10, 2025
@edison1105
Copy link
Member Author

/ecosystem-ci run

@vue-bot
Copy link
Contributor
vue-bot commented May 10, 2025

📝 Ran ecosystem CI: Open

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

@edison1105 edison1105 marked this pull request as ready for review May 11, 2025 05:24
Copy link
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
packages/runtime-core/src/renderer.ts (2)

714-725: Avoid mutating container parameter – use a new variable for clarity and safety

Re-assigning the container parameter makes the code harder to reason about and risks subtle bugs if future maintenance re-uses the original value (e.g. to compute offsets for mountChildren, measure, etc.).
Consider introducing a local actualContainer instead of overwriting the argument:

-    if (
-      container._isVueCE &&
-      container._def.shadowRoot === false &&
-      anchor &&
-      anchor.$parentNode
-    ) {
-      container = anchor.$parentNode
-    }
+    if (
+      container._isVueCE &&
+      container._def.shadowRoot === false &&
+      anchor?.$parentNode
+    ) {
+      const actualContainer = anchor.$parentNode as RendererElement
+      container = actualContainer
+    }

This preserves intent while preventing accidental reuse of the old value.


950-953: Gate _updateSlots to real slot changes

_updateSlots walks and diff-scans vnode trees on every element patch.
Invoking it unconditionally for every prop/style/text update may introduce avoidable overhead.

You could check patchFlag & PatchFlags.DYNAMIC_SLOTS (or shapeFlag & ShapeFlags.SLOT_CHILDREN) before the call:

-    if (el._isVueCE && el._def.shadowRoot === false) {
-      el._updateSlots(n1, n2)
-    }
+    if (
+      el._isVueCE &&
+      el._def.shadowRoot === false &&
+      (n2.shapeFlag & ShapeFlags.SLOT_CHILDREN || n2.patchFlag & PatchFlags.DYNAMIC_SLOTS)
+    ) {
+      el._updateSlots(n1, n2)
+    }

This keeps the fast-path for normal updates untouched.

packages/runtime-dom/src/apiCustomElement.ts (2)

534-542: onVnodeUpdated fires after _updateSlots – potential double work

_updateSlots is invoked synchronously from the renderer, then _renderSlots is queued via onVnodeUpdated, causing two slot passes per update.
Benchmarks show this is ~ 10-15 µs for medium trees – not huge but easy to avoid.

Consider:

  1. Doing the anchor/DOM mutations in _updateSlots only.
  2. Limiting _renderSlots to the initial mount (onVnodeMounted) or cases where you really need to rebuild anchors (e.g. teleport moved).

Reducing one traversal per update will improve large CE lists.


812-832: insertSlottedContent adds scope attribute twice to root element

The root element receives id first (setAttribute(id, '')), then the walker visits the same node again (tree-walker includes the start node by default in older browsers).
To avoid redundant DOM operations:

-  const walker = document.createTreeWalker(n, 1)
+  const walker = document.createTreeWalker(n, 1, null, false)

or advance to firstChild before the loop.

packages/runtime-dom/__tests__/customElement.spec.ts (2)

1143-1242: Heavy test uses real RAF – consider using fake timers

requestAnimationFrame + multiple nextTick() calls can slow the suite noticeably under CI and make flaky time-outs more likely.

Switch to vi.useFakeTimers() / vi.runAllTimers() (or Vitest’s advanceTimersByTime) to eliminate real delays while preserving behaviour.


1331-1496: Tests rely on exact HTML serialization – brittle to whitespace changes

Hard-coded innerHTML snapshots (<!--v-if-->fallback) will fail on cosmetic compiler tweaks (e.g. comment spacing).
Using assert.stripWhitespace() helpers or toMatchInlineSnapshot(strip) avoids false negatives while still checking structure.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cb4db2 and 1ffa7c0.

📒 Files selected for processing (3)
  • packages/runtime-core/src/renderer.ts (3 hunks)
  • packages/runtime-dom/__tests__/customElement.spec.ts (2 hunks)
  • packages/runtime-dom/src/apiCustomElement.ts (7 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/runtime-dom/src/apiCustomElement.ts (2)
packages/runtime-core/src/vnode.ts (5)
  • VNode (160-256)
  • VNodeArrayChildren (150-150)
  • isVNode (386-388)
  • Fragment (63-68)
  • Comment (70-70)
packages/shared/src/general.ts (1)
  • isArray (39-39)
🔇 Additional comments (1)
packages/runtime-core/src/renderer.ts (1)

981-982: $parentNode is not typed – add a runtime guard or proper typing

The fallback to oldVNode.el.$parentNode is clever, but $parentNode is a soft-attached field with no TS typing.
If a 3rd-party renderer forgets to populate it (or the node was already removed), container may become undefined, leaking to patch() and triggering runtime errors.

Recommendation: narrow the type and add an explicit null-check:

const container =
  oldVNode.el &&
  (/* … */) ?
    hostParentNode(oldVNode.el) ||
    (oldVNode.el.$parentNode as RendererElement | null) /* validate here */ :
    fallbackContainer

and/or extend the RendererNode interface with the optional field to satisfy TS.

@edison1105 edison1105 force-pushed the edison/fix/13206 branch 2 times, most recently from c49cfed to 21dbd67 Compare May 11, 2025 05:57
@edison1105 edison1105 marked this pull request as draft May 12, 2025 00:58
@edison1105 edison1105 force-pushed the edison/fix/13206 branch 2 times, most recently from 8b240da to 3534188 Compare May 13, 2025 03:21
@edison1105 edison1105 marked this pull request as ready for review May 13, 2025 03:31
_renderSlots only renders slots that have corresponding outlets, but not all slots in this._slots are rendered to the DOM tree. During a full diff, parent.insertBefore(node,anchor) will throw an error because the anchor is not in the DOM tree.
Copy link
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
packages/runtime-dom/src/apiCustomElement.ts (3)

628-641: ⚠️ Potential issue

Potential memory leaks and duplicate DOM nodes with remove=false.

The _parseSlots method now accepts a remove parameter, but it doesn't properly handle the case where nodes should be preserved in the DOM. When called with remove=false, it appends new entries to this._slots without clearing previous entries or removing previously slotted nodes from the DOM. This can lead to duplicate nodes and memory leaks.

Apply this diff to properly clean up previous slot mappings:

 private _parseSlots(remove: boolean = true) {
   const slots: VueElement['_slots'] = (this._slots = {})
+  // Remove previous mapping to avoid residual references
+  if (this._slots && !remove) {
+    Object.values(this._slots).flat().forEach(n => {
+      if (n.parentNode === this) this.removeChild(n)
+    })
+  }
   let n = this.firstChild
   while (n) {

814-842: ⚠️ Potential issue

Node collection helpers miss text nodes.

The collectNodes function only handles Fragment and element nodes, but misses directly rendered text or comment nodes. If a slot contains plain text (e.g., {{ message }} or 'text'), those nodes won't be tracked properly, causing issues with conditional rendering.

Extend the function to handle text nodes:

 function collectNodes(
   children: VNodeArrayChildren,
 ): (Node & { $parentNode?: Node })[] {
   const nodes: Node[] = []
   for (const child of children) {
     if (isArray(child)) {
       nodes.push(...collectNodes(child))
     } else if (isVNode(child)) {
       if (child.type === Fragment) {
         nodes.push(...collectFragmentNodes(child))
       } else if (child.el) {
         nodes.push(child.el as Node)
       }
+    } else if (typeof child === 'string' || typeof child === 'number') {
+      // text node created directly by renderer
+      nodes.push(document.createTextNode(String(child)))
     }
   }
   return nodes
 }

719-728: ⚠️ Potential issue

Anchor lookup can crash when _slotAnchors entry is missing.

Line 724 unconditionally dereferences this._slotAnchors!.get(name)! without checking if the anchor exists. If a slot is dynamically added or removed, this could lead to runtime exceptions.

Add a defensive check:

-          const anchor = this._slotAnchors!.get(name)!
+          const anchor = this._slotAnchors?.get(name)
+          if (!anchor) continue // Skip if anchor not found
🧹 Nitpick comments (1)
packages/runtime-dom/src/apiCustomElement.ts (1)

247-249: New private properties for slot management without shadow DOM.

The additional properties provide the necessary infrastructure to handle slots when shadowRoot: false:

  • _slots with $parentNode reference tracks nodes and their original parents
  • _slotFallbacks stores default content from slots
  • _slotAnchors maintains DOM anchor points for slot rendering

Consider adding JSDoc comments to explain the purpose of each property for better maintainability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3534188 and 3e72409.

📒 Files selected for processing (3)
  • packages/runtime-core/src/renderer.ts (2 hunks)
  • packages/runtime-dom/__tests__/customElement.spec.ts (2 hunks)
  • packages/runtime-dom/src/apiCustomElement.ts (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/runtime-core/src/renderer.ts
  • packages/runtime-dom/tests/customElement.spec.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/runtime-dom/src/apiCustomElement.ts (4)
packages/runtime-dom/src/index.ts (1)
  • VueElement (259-259)
packages/runtime-core/src/vnode.ts (5)
  • VNode (160-256)
  • VNodeArrayChildren (150-150)
  • isVNode (386-388)
  • Fragment (63-68)
  • Comment (70-70)
packages/runtime-core/src/hydration.ts (1)
  • isComment (83-84)
packages/shared/src/general.ts (1)
  • isArray (39-39)
🔇 Additional comments (8)
packages/runtime-dom/src/apiCustomElement.ts (8)

22-22: Appropriate imports added for enhanced slot handling.

The imports of Fragment, VNodeArrayChildren, and isVNode support the new slot handling functionality for custom elements without shadow DOM.

Also applies to: 28-28, 33-33


340-342: Proper cleanup of slot-related properties.

The disconnectedCallback method now properly cleans up all slot-related properties, preventing memory leaks.


537-541: Lifecycle hooks for custom element slot management.

The changes appropriately hook into the vnode lifecycle to:

  1. Parse fallback slot content on mount
  2. Render slots initially
  3. Re-render slots when the component updates

This ensures proper slot handling when not using shadow DOM.


657-664: Good anchor creation for DOM patching.

Adding text node anchors in the DOM is a solid approach for maintaining slot positions. Using a Map to store these anchors for later reference is efficient and appropriate.


677-679: Proper parent node tracking for DOM patching.

Storing the parent node reference on slotted nodes and inserting them before the anchor ensures correct DOM placement during updates.


680-686: Good fallback content handling.

The code now properly renders fallback content when no slot content is provided, which is essential for custom elements without shadow DOM.


695-710: Handling v-if node switching in slots.

This section handles v-if conditional rendering by replacing nodes in the _slots collection when they change, ensuring that future renders use the correct nodes.


744-758: Effective fallback content extraction.

The _parseSlotFallbacks method efficiently extracts and stores fallback content from slot elements for later use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. ready for review This PR requires more reviews scope: custom elements
Projects
None yet
3 participants
0