Parcourir la source

fix(runtime-vapor): respect v-once in vdom slot interop

daiwei il y a 3 semaines
Parent
commit
35b68cac2b

+ 1 - 0
packages/runtime-core/src/apiCreateApp.ts

@@ -250,6 +250,7 @@ export interface VaporInteropInterface {
     props: Record<string, any>,
     parentComponent: any, // VaporComponentInstance
     fallback?: any, // VaporSlot
+    once?: boolean,
   ) => any
   vdomMountVNode: (
     vnode: VNode,

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

@@ -1807,6 +1807,37 @@ describe('component: slots', () => {
       await nextTick()
       expect(html()).toBe('<div>0</div><!--slot-->')
     })
+
+    test('applies v-once to fallback', async () => {
+      const count = ref(0)
+      const Child = defineVaporComponent({
+        setup() {
+          return createSlot(
+            'default',
+            null,
+            () => {
+              const n3 = template('<div> </div>')() as any
+              const x3 = txt(n3) as any
+              renderEffect(() => setText(x3, toDisplayString(count.value)))
+              return n3
+            },
+            VaporSlotFlags.ONCE,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return createComponent(Child)
+        },
+      }).render()
+
+      expect(html()).toBe('<div>0</div><!--slot-->')
+
+      count.value++
+      await nextTick()
+      expect(html()).toBe('<div>0</div><!--slot-->')
+    })
   })
 
   describe('forwarded slot', () => {

+ 160 - 0
packages/runtime-vapor/__tests__/vdomInterop.spec.ts

@@ -32,6 +32,7 @@ import {
   withCtx,
   withDirectives,
 } from '@vue/runtime-dom'
+import { VaporSlotFlags } from '@vue/shared'
 import { VaporSlot } from '../../runtime-core/src/vnode'
 import { compile, makeInteropRender } from './_utils'
 import {
@@ -1278,6 +1279,165 @@ describe('vdomInterop', () => {
       expect(html()).toBe('<span>updated</span>')
     })
 
+    test('applies v-once to VDOM slot content passed to Vapor', async () => {
+      const msg = ref('default slot')
+      const VaporChild = defineVaporComponent(() =>
+        createSlot('default', null, undefined, VaporSlotFlags.ONCE),
+      )
+
+      const { html } = define({
+        setup() {
+          return () =>
+            h(VaporChild as any, null, {
+              default: () => h('span', msg.value),
+            })
+        },
+      }).render()
+
+      expect(html()).toBe('<span>default slot</span>')
+
+      msg.value = 'updated'
+      await nextTick()
+      expect(html()).toBe('<span>default slot</span>')
+    })
+
+    test('applies v-once to dynamic VDOM slot names passed to Vapor', async () => {
+      const slotName = ref('one')
+      const VaporChild = defineVaporComponent(() =>
+        createSlot(() => slotName.value, null, undefined, VaporSlotFlags.ONCE),
+      )
+
+      const { html } = define({
+        setup() {
+          return () =>
+            h(VaporChild as any, null, {
+              one: () => h('span', 'one'),
+              two: () => h('span', 'two'),
+            })
+        },
+      }).render()
+
+      expect(html()).toBe('<span>one</span>')
+
+      slotName.value = 'two'
+      await nextTick()
+      expect(html()).toBe('<span>one</span>')
+    })
+
+    test('applies v-once when VDOM slot availability changes', async () => {
+      const show = ref(true)
+      const VaporChild = defineVaporComponent(() =>
+        createSlot('default', null, undefined, VaporSlotFlags.ONCE),
+      )
+
+      const { html } = define({
+        setup() {
+          return () =>
+            h(
+              VaporChild as any,
+              null,
+              show.value ? { default: () => h('span', 'one') } : {},
+            )
+        },
+      }).render()
+
+      expect(html()).toBe('<span>one</span>')
+
+      show.value = false
+      await nextTick()
+      expect(html()).toBe('<span>one</span>')
+    })
+
+    test('applies v-once to VDOM slot outlet fallback', async () => {
+      const msg = ref('fallback')
+      const VaporChild = defineVaporComponent(() =>
+        createSlot(
+          'default',
+          null,
+          () => {
+            const n0 = template('<span> </span>')() as any
+            const t0 = txt(n0) as any
+            renderEffect(() => setText(t0, msg.value))
+            return n0
+          },
+          VaporSlotFlags.ONCE,
+        ),
+      )
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      expect(html()).toBe('<span>fallback</span>')
+
+      msg.value = 'updated'
+      await nextTick()
+      expect(html()).toBe('<span>fallback</span>')
+    })
+
+    test('preserves VDOM child updates inside v-once slot content', async () => {
+      let increment!: () => void
+      const VDomChild = defineComponent({
+        setup() {
+          const count = ref(0)
+          increment = () => count.value++
+          return () => h('span', count.value)
+        },
+      })
+      const VaporChild = defineVaporComponent(() =>
+        createSlot('default', null, undefined, VaporSlotFlags.ONCE),
+      )
+
+      const { html } = define({
+        setup() {
+          return () =>
+            h(VaporChild as any, null, {
+              default: () => h(VDomChild),
+            })
+        },
+      }).render()
+
+      expect(html()).toBe('<span>0</span>')
+
+      increment()
+      await nextTick()
+      expect(html()).toBe('<span>1</span>')
+    })
+
+    test('preserves Vapor child updates inside v-once VDOM slot content', async () => {
+      let increment!: () => void
+      const VaporGrandChild = defineVaporComponent({
+        setup() {
+          const count = ref(0)
+          increment = () => count.value++
+          const n0 = template('<span> </span>')() as any
+          const t0 = txt(n0) as any
+          renderEffect(() => setText(t0, String(count.value)))
+          return n0
+        },
+      })
+      const VaporChild = defineVaporComponent(() =>
+        createSlot('default', null, undefined, VaporSlotFlags.ONCE),
+      )
+
+      const { html } = define({
+        setup() {
+          return () =>
+            h(VaporChild as any, null, {
+              default: () => h(VaporGrandChild as any),
+            })
+        },
+      }).render()
+
+      expect(html()).toBe('<span>0</span>')
+
+      increment()
+      await nextTick()
+      expect(html()).toBe('<span>1</span>')
+    })
+
     test('falls through to outlet fallback when vdom local fallback is invalidated or removed from VaporSlot', async () => {
       const mode = ref<'local' | 'empty' | 'none'>('local')
       const localText = ref('local fallback')

+ 17 - 5
packages/runtime-vapor/src/componentSlots.ts

@@ -47,6 +47,16 @@ import { setScopeId } from './scopeId'
  */
 export let inOnceSlot = false
 
+export function withOnceSlot<T>(fn: () => T): T {
+  const prev = inOnceSlot
+  try {
+    inOnceSlot = true
+    return fn()
+  } finally {
+    inOnceSlot = prev
+  }
+}
+
 /**
  * Current slot scopeIds for vdom interop
  */
@@ -209,6 +219,11 @@ export function createSlot(
   const scopeId =
     !(flags & VaporSlotFlags.NO_SLOTTED) && instance.type.__scopeId
   const slotScopeIds = scopeId ? [`${scopeId}-s`] : null
+  const once = !!(flags & VaporSlotFlags.ONCE)
+  if (once && fallback) {
+    const originalFallback = fallback
+    fallback = (...args: any[]) => withOnceSlot(() => originalFallback(...args))
+  }
 
   let fragment: VaporFragment
   if (isRef(rawSlots._) && isInteropEnabled) {
@@ -219,6 +234,7 @@ export function createSlot(
       slotProps,
       instance,
       fallback,
+      once,
     )
   } else {
     if (isHydrating) hydrationCursor = captureHydrationCursor()
@@ -227,7 +243,6 @@ export function createSlot(
     slotFragment.forwarded =
       currentSlotOwner != null && currentSlotOwner !== currentInstance
     const isDynamicName = isFunction(name)
-    const once = !!(flags & VaporSlotFlags.ONCE)
 
     const renderSlot = () => {
       const slotName = isFunction(name) ? name() : name
@@ -277,12 +292,9 @@ export function createSlot(
         cachedSlot = slot
         cachedBoundSlot = () => {
           const prevSlotScopeIds = setCurrentSlotScopeIds(slotScopeIds)
-          const prev = inOnceSlot
           try {
-            if (once) inOnceSlot = true
-            return slot(slotProps)
+            return once ? withOnceSlot(() => slot(slotProps)) : slot(slotProps)
           } finally {
-            inOnceSlot = prev
             setCurrentSlotScopeIds(prevSlotScopeIds)
           }
         }

+ 21 - 9
packages/runtime-vapor/src/vdomInterop.ts

@@ -85,7 +85,11 @@ import {
 } from '@vue/shared'
 import { type RawProps, rawPropsProxyHandlers } from './componentProps'
 import type { RawSlots, VaporSlot } from './componentSlots'
-import { currentSlotScopeIds, setCurrentSlotOwner } from './componentSlots'
+import {
+  currentSlotScopeIds,
+  setCurrentSlotOwner,
+  withOnceSlot,
+} from './componentSlots'
 import { renderEffect } from './renderEffect'
 import { _next, createTextNode } from './dom/node'
 import { optimizePropertyLookup } from './dom/prop'
@@ -1365,6 +1369,7 @@ function renderVDOMSlot(
   props: Record<string, any>,
   parentComponent: VaporComponentInstance,
   fallback?: VaporSlot,
+  once?: boolean,
 ): VaporFragment {
   const suspense = currentParentSuspense || parentComponent.suspense
   const frag = new VaporFragment<Block>([])
@@ -1415,7 +1420,9 @@ function renderVDOMSlot(
     },
   }
   localFallback = fallback
-    ? () => fallback(internals, parentComponent)
+    ? once
+      ? () => withOnceSlot(() => fallback(internals, parentComponent))
+      : () => fallback(internals, parentComponent)
     : undefined
 
   const setRenderedContent = (rendered: VNode | Block | null): void => {
@@ -1504,18 +1511,22 @@ function renderVDOMSlot(
     const prev = currentInstance
     simpleSetCurrentInstance(instance)
     try {
-      renderEffect(() => {
+      const renderSlotContent = () => {
         runWithFragmentRenderCtx(frag, () =>
           withOwnedSlotBoundary(boundary, () => {
             let slotContent: VNode | Block | undefined
             let slotContentValid = false
 
             if (slotsRef.value) {
-              slotContent = renderSlot(
-                slotsRef.value,
-                isFunction(name) ? name() : name,
-                props,
-              )
+              const renderSlotContent = () =>
+                renderSlot(
+                  slotsRef.value,
+                  isFunction(name) ? name() : name,
+                  props,
+                )
+              slotContent = once
+                ? withOnceSlot(renderSlotContent)
+                : renderSlotContent()
 
               if (isVNode(slotContent)) {
                 if (slotContent.type === Fragment) {
@@ -1639,7 +1650,8 @@ function renderVDOMSlot(
             finishContentUpdate()
           }),
         )
-      })
+      }
+      once ? renderSlotContent() : renderEffect(renderSlotContent)
     } finally {
       simpleSetCurrentInstance(prev)
     }