Ver Fonte

fix(runtime-vapor): track and restore slot owner context for DynamicFragment rendering (#14193)

close #14192
edison há 4 meses atrás
pai
commit
79aa9dbf68

+ 66 - 0
packages/runtime-vapor/__tests__/componentSlots.spec.ts

@@ -909,6 +909,72 @@ describe('component: slots', () => {
       expect(html()).toBe('child fallback<!--slot--><!--slot-->')
     })
 
+    test('named forwarded slot with v-if', async () => {
+      const Child = defineVaporComponent({
+        setup() {
+          return createSlot('default', null)
+        },
+      })
+
+      const Parent = defineVaporComponent({
+        props: {
+          show: Boolean,
+        },
+        setup(props) {
+          const n6 = createComponent(
+            Child,
+            null,
+            {
+              default: withVaporCtx(() => {
+                const n0 = createIf(
+                  () => props.show,
+                  () => {
+                    const n5 = template('<div></div>')() as any
+                    setInsertionState(n5, null, true)
+                    createSlot('header', null, () => {
+                      const n4 = template('default header')()
+                      return n4
+                    })
+                    return n5
+                  },
+                )
+                return n0
+              }),
+            },
+            true,
+          )
+          return n6
+        },
+      })
+
+      const show = ref(false)
+      const { html } = define({
+        setup() {
+          return createComponent(
+            Parent,
+            {
+              show: () => show.value,
+            },
+            {
+              header: () => template('custom header')(),
+            },
+          )
+        },
+      }).render()
+
+      expect(html()).toBe('<!--if--><!--slot-->')
+
+      show.value = true
+      await nextTick()
+      expect(html()).toBe(
+        '<div>custom header<!--slot--></div><!--if--><!--slot-->',
+      )
+
+      show.value = false
+      await nextTick()
+      expect(html()).toBe('<!--if--><!--slot-->')
+    })
+
     test('forwarded slot with fallback (v-if)', async () => {
       const Child = defineVaporComponent({
         setup() {

+ 15 - 6
packages/runtime-vapor/src/fragment.ts

@@ -34,6 +34,7 @@ import {
 } from './dom/hydration'
 import { isArray } from '@vue/shared'
 import { renderEffect } from './renderEffect'
+import { currentSlotOwner, setCurrentSlotOwner } from './componentSlots'
 
 export class VaporFragment<T extends Block = Block>
   implements TransitionOptions
@@ -97,8 +98,11 @@ export class DynamicFragment extends VaporFragment {
   ) => boolean)[]
   onBeforeMount?: ((newKey: any, nodes: Block, scope: EffectScope) => void)[]
 
+  slotOwner: VaporComponentInstance | null
+
   constructor(anchorLabel?: string) {
     super([])
+    this.slotOwner = currentSlotOwner
     if (isHydrating) {
       this.anchorLabel = anchorLabel
       locateHydrationNode()
@@ -204,12 +208,14 @@ export class DynamicFragment extends VaporFragment {
         this.scope = new EffectScope()
       }
 
+      // restore slot owner
+      const prevOwner = setCurrentSlotOwner(this.slotOwner)
       // switch current instance to parent instance during update
       // ensure that the parent instance is correct for nested components
-      let prev
-      if (parent && instance) prev = setCurrentInstance(instance)
+      const prev = parent && instance ? setCurrentInstance(instance) : undefined
       this.nodes = this.scope.run(render) || []
-      if (parent && instance) setCurrentInstance(...prev!)
+      if (prev !== undefined) setCurrentInstance(...prev)
+      setCurrentSlotOwner(prevOwner)
 
       if (transition) {
         this.$transition = applyTransitionHooks(this.nodes, transition)
@@ -225,9 +231,12 @@ export class DynamicFragment extends VaporFragment {
         // apply fallthrough props during update
         if (this.attrs) {
           if (this.nodes instanceof Element) {
-            renderEffect(() =>
-              applyFallthroughProps(this.nodes as Element, this.attrs!),
-            )
+            // ensure render effect is cleaned up when scope is stopped
+            this.scope.run(() => {
+              renderEffect(() =>
+                applyFallthroughProps(this.nodes as Element, this.attrs!),
+              )
+            })
           } else if (
             __DEV__ &&
             // preventing attrs fallthrough on slots