Przeglądaj źródła

feat(vapor): support rendering vdom suspense in vapor (#14485)

edison 1 miesiąc temu
rodzic
commit
f0367b0e0c

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

@@ -205,6 +205,7 @@ export interface VaporInteropInterface {
     container: any,
     anchor: any,
     parentComponent: ComponentInternalInstance | null,
+    parentSuspense: SuspenseBoundary | null,
   ): void
   hydrate(
     vnode: VNode,

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

@@ -465,6 +465,7 @@ function baseCreateRenderer(
           container,
           anchor,
           parentComponent,
+          parentSuspense,
         )
         break
       default:

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

@@ -1,6 +1,7 @@
 import {
   KeepAlive,
   type ShallowRef,
+  Suspense,
   Teleport,
   createApp,
   createVNode,
@@ -1464,4 +1465,191 @@ describe('vdomInterop', () => {
       }
     })
   })
+
+  describe('Suspense', () => {
+    test('renders async vapor child inside VDOM Suspense', async () => {
+      const duration = 5
+
+      const VaporAsyncChild = defineVaporComponent({
+        async setup() {
+          await new Promise(resolve => setTimeout(resolve, duration))
+          return template('<div><button>click</button></div>')()
+        },
+      })
+
+      const VaporParent = defineVaporComponent({
+        setup() {
+          return createComponent(
+            Suspense as any,
+            null,
+            {
+              default: () => createComponent(VaporAsyncChild, null, null, true),
+              fallback: () => template('loading')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporParent as any)
+        },
+      }).render()
+
+      expect(html()).toContain('loading')
+
+      await new Promise(resolve => setTimeout(resolve, duration + 1))
+      await nextTick()
+
+      expect(html()).toContain('<div><button>click</button></div>')
+    })
+
+    test('renders async VDOM child inside VDOM Suspense', async () => {
+      const duration = 5
+
+      const VDomAsyncChild = defineComponent({
+        async setup() {
+          await new Promise(resolve => setTimeout(resolve, duration))
+          return () => h('div', [h('button', 'click')])
+        },
+      })
+
+      const VaporParent = defineVaporComponent({
+        setup() {
+          return createComponent(
+            Suspense as any,
+            null,
+            {
+              default: () =>
+                createComponent(VDomAsyncChild as any, null, null, true),
+              fallback: () => template('loading')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporParent as any)
+        },
+      }).render()
+
+      expect(html()).toContain('loading')
+
+      await new Promise(resolve => setTimeout(resolve, duration + 1))
+      await nextTick()
+
+      expect(html()).toContain('<div><button>click</button></div>')
+    })
+
+    test('renders async VDOM child from vapor slot outlet inside VDOM Suspense', async () => {
+      const duration = 5
+
+      const VaporSlotOutlet = defineVaporComponent({
+        setup() {
+          return createSlot('default')
+        },
+      })
+
+      const VDomAsyncChild = defineComponent({
+        async setup() {
+          await new Promise(resolve => setTimeout(resolve, duration))
+          return () => h('div', 'slot async')
+        },
+      })
+
+      const App = defineComponent({
+        setup() {
+          return () =>
+            h(Suspense, null, {
+              default: () =>
+                h(VaporSlotOutlet as any, null, {
+                  default: () => [h(VDomAsyncChild as any)],
+                }),
+              fallback: () => h('div', 'loading'),
+            })
+        },
+      })
+
+      const { html } = define(App).render()
+
+      expect(html()).toContain('loading')
+
+      await new Promise(resolve => setTimeout(resolve, duration + 1))
+      await nextTick()
+
+      expect(html()).toContain('<div>slot async</div>')
+    })
+
+    test('renders async VDOM vnode via createDynamicComponent inside VDOM Suspense', async () => {
+      const duration = 5
+
+      const VDomAsyncChild = defineComponent({
+        async setup() {
+          await new Promise(resolve => setTimeout(resolve, duration))
+          return () => h('button', 'vnode async')
+        },
+      })
+
+      const VaporParent = defineVaporComponent({
+        setup() {
+          return createComponent(
+            Suspense as any,
+            null,
+            {
+              default: () =>
+                createDynamicComponent(
+                  () => h(VDomAsyncChild as any),
+                  null,
+                  null,
+                  true,
+                ),
+              fallback: () => template('loading')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporParent as any)
+        },
+      }).render()
+
+      expect(html()).toContain('loading')
+
+      await new Promise(resolve => setTimeout(resolve, duration + 1))
+      await nextTick()
+
+      expect(html()).toContain('<button>vnode async</button>')
+    })
+
+    test('mounts VDOM Suspense from createDynamicComponent', async () => {
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return createDynamicComponent(
+            () => Suspense,
+            null,
+            {
+              default: () => template('<span>resolved</span>')(),
+              fallback: () => template('<span>fallback</span>')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      await nextTick()
+      expect(html()).toContain('<span>resolved</span>')
+    })
+  })
 })

+ 27 - 5
packages/runtime-vapor/src/vdomInterop.ts

@@ -100,7 +100,10 @@ import {
   deactivate,
   setCurrentKeepAliveCtx,
 } from './components/KeepAlive'
-import { setParentSuspense } from './components/Suspense'
+import {
+  parentSuspense as currentParentSuspense,
+  setParentSuspense,
+} from './components/Suspense'
 
 export const interopKey: unique symbol = Symbol(`interop`)
 
@@ -246,10 +249,21 @@ const vaporInteropImpl: Omit<
   /**
    * vapor slot in vdom
    */
-  slot(n1: VNode, n2: VNode, container, anchor, parentComponent) {
+  slot(
+    n1: VNode,
+    n2: VNode,
+    container,
+    anchor,
+    parentComponent,
+    parentSuspense,
+  ) {
     if (!n1) {
       const prev = currentInstance
+      let prevSuspense: SuspenseBoundary | null = null
       simpleSetCurrentInstance(parentComponent)
+      if (__FEATURE_SUSPENSE__ && parentSuspense) {
+        prevSuspense = setParentSuspense(parentSuspense)
+      }
       // mount
       let selfAnchor: Node | undefined
       const { slot, fallback } = n2.vs!
@@ -266,6 +280,9 @@ const vaporInteropImpl: Omit<
         // use fragment's anchor when possible
         selfAnchor = slotBlock.anchor
       }
+      if (__FEATURE_SUSPENSE__ && parentSuspense) {
+        setParentSuspense(prevSuspense)
+      }
       simpleSetCurrentInstance(prev)
       if (!selfAnchor) selfAnchor = createTextNode()
       insert((n2.el = n2.anchor = selfAnchor), container, anchor)
@@ -374,6 +391,8 @@ function mountVNode(
   vnode: VNode,
   parentComponent: VaporComponentInstance | null,
 ): VaporFragment {
+  const suspense =
+    currentParentSuspense || (parentComponent && parentComponent.suspense)
   const frag = new VaporFragment([])
   frag.vnode = vnode
 
@@ -437,7 +456,7 @@ function mountVNode(
           parentNode,
           anchor,
           parentComponent as any,
-          null, // parentSuspense
+          suspense,
           undefined, // namespace
           vnode.slotScopeIds,
         )
@@ -474,6 +493,8 @@ function createVDOMComponent(
   rawSlots?: LooseRawSlots | null,
   isSingleRoot?: boolean,
 ): VaporFragment {
+  const suspense =
+    currentParentSuspense || (parentComponent && parentComponent.suspense)
   const useBridge = shouldUseRendererBridge(component)
   const comp = useBridge ? ensureRendererBridge(component) : component
   const frag = new VaporFragment([])
@@ -572,7 +593,7 @@ function createVDOMComponent(
           parentNode,
           anchor,
           parentComponent as any,
-          null,
+          suspense,
           undefined,
           false,
         )
@@ -667,6 +688,7 @@ function renderVDOMSlot(
   parentComponent: VaporComponentInstance,
   fallback?: VaporSlot,
 ): VaporFragment {
+  const suspense = currentParentSuspense || parentComponent.suspense
   const frag = new VaporFragment([])
 
   if (fallback && !frag.fallback) frag.fallback = fallback
@@ -785,7 +807,7 @@ function renderVDOMSlot(
           parentNode!,
           anchor,
           parentComponent as any,
-          null, // parentSuspense
+          suspense,
           undefined, // namespace
           resolved.slotScopeIds, // pass slotScopeIds for :slotted styles
         )