]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-vapor): vapor transition work with vapor async component (#14053)
authoredison <daiwei521@126.com>
Wed, 5 Nov 2025 03:42:28 +0000 (11:42 +0800)
committerGitHub <noreply@github.com>
Wed, 5 Nov 2025 03:42:28 +0000 (11:42 +0800)
packages-private/vapor-e2e-test/__tests__/transition.spec.ts
packages-private/vapor-e2e-test/transition/App.vue
packages/runtime-vapor/src/apiDefineAsyncComponent.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/components/Transition.ts

index 180b0157aaa0aed6a2d349d9133cb3b8ef961b6a..1dce93782e1e5107df8e05ea76e6cef0995efeef 100644 (file)
@@ -13,7 +13,6 @@ const {
   nextFrame,
   timeout,
   isVisible,
-  count,
   html,
   transitionStart,
   waitForElement,
@@ -40,6 +39,9 @@ describe('vapor transition', () => {
 
   beforeEach(async () => {
     const baseUrl = `http://localhost:${port}/transition/`
+    await page().evaluateOnNewDocument(dur => {
+      ;(window as any).__TRANSITION_DURATION__ = dur
+    }, duration)
     await page().goto(baseUrl)
     await page().waitForSelector('#app')
   })
@@ -972,6 +974,65 @@ describe('vapor transition', () => {
     )
   })
 
+  describe('transition with AsyncComponent', () => {
+    test('apply transition to inner component', async () => {
+      const btnSelector = '.async > button'
+      const containerSelector = '.async > div'
+
+      expect(await html(containerSelector)).toBe('')
+
+      // toggle
+      await click(btnSelector)
+      await nextTick()
+      // not yet resolved
+      expect(await html(containerSelector)).toBe('')
+
+      // wait resolving
+      await timeout(50)
+
+      // enter (resolved)
+      expect(await html(containerSelector)).toBe(
+        '<div class="v-enter-from v-enter-active">vapor compA</div>',
+      )
+      await nextFrame()
+      expect(await html(containerSelector)).toBe(
+        '<div class="v-enter-active v-enter-to">vapor compA</div>',
+      )
+      await transitionFinish()
+      expect(await html(containerSelector)).toBe(
+        '<div class="">vapor compA</div>',
+      )
+
+      // leave
+      await click(btnSelector)
+      await nextTick()
+      expect(await html(containerSelector)).toBe(
+        '<div class="v-leave-from v-leave-active">vapor compA</div>',
+      )
+      await nextFrame()
+      expect(await html(containerSelector)).toBe(
+        '<div class="v-leave-active v-leave-to">vapor compA</div>',
+      )
+      await transitionFinish()
+      expect(await html(containerSelector)).toBe('')
+
+      // enter again
+      await click(btnSelector)
+      // use the already resolved component
+      expect(await html(containerSelector)).toBe(
+        '<div class="v-enter-from v-enter-active">vapor compA</div>',
+      )
+      await nextFrame()
+      expect(await html(containerSelector)).toBe(
+        '<div class="v-enter-active v-enter-to">vapor compA</div>',
+      )
+      await transitionFinish()
+      expect(await html(containerSelector)).toBe(
+        '<div class="">vapor compA</div>',
+      )
+    })
+  })
+
   describe('transition with v-show', () => {
     test(
       'named transition with v-show',
index 8b07a5ac4be4da909138060cb30adf310ebfec5b..e7227ded01e7c11369cc2b99821b89e7002e0c16 100644 (file)
@@ -7,6 +7,7 @@ import {
   VaporTransition,
   createIf,
   template,
+  defineVaporAsyncComponent,
   onUnmounted,
 } from 'vue'
 const show = ref(true)
@@ -14,7 +15,7 @@ const toggle = ref(true)
 const count = ref(0)
 
 const timeout = (fn, time) => setTimeout(fn, time)
-const duration = typeof process !== 'undefined' && process.env.CI ? 200 : 50
+const duration = window.__TRANSITION_DURATION__ || 50
 
 let calls = {
   basic: [],
@@ -94,6 +95,10 @@ function changeViewInOut() {
   viewInOut.value = viewInOut.value === SimpleOne ? Two : SimpleOne
 }
 
+const AsyncComp = defineVaporAsyncComponent(() => {
+  return new Promise(resolve => setTimeout(() => resolve(VaporCompA), 50))
+})
+
 const TrueBranch = defineVaporComponent({
   name: 'TrueBranch',
   setup() {
@@ -503,6 +508,17 @@ const click = () => {
     </div>
     <!-- mode end -->
 
+    <!-- async component -->
+    <div class="async">
+      <div id="container">
+        <transition>
+          <AsyncComp v-if="!toggle"></AsyncComp>
+        </transition>
+      </div>
+      <button @click="toggle = !toggle">button</button>
+    </div>
+    <!-- async component end -->
+
     <!-- with teleport -->
     <div class="with-teleport">
       <div class="target"></div>
index dd6143950e349daf74e3d91e86829752bf9c8083..4cde9454f0953e120739ee58c4058cfae1a19af5 100644 (file)
@@ -26,8 +26,9 @@ import {
   removeFragmentNodes,
 } from './dom/hydration'
 import { invokeArrayFns } from '@vue/shared'
-import { insert, remove } from './block'
+import { type TransitionOptions, insert, remove } from './block'
 import { parentNode } from './dom/node'
+import { setTransitionHooks } from './components/Transition'
 
 /*@ __NO_SIDE_EFFECTS__ */
 export function defineVaporAsyncComponent<T extends VaporComponent>(
@@ -109,7 +110,8 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
     },
 
     setup() {
-      const instance = currentInstance as VaporComponentInstance
+      const instance = currentInstance as VaporComponentInstance &
+        TransitionOptions
       markAsyncBoundary(instance)
 
       const frag =
@@ -166,6 +168,8 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
         } else if (loadingComponent && !delayed.value) {
           render = () => createComponent(loadingComponent)
         }
+
+        if (instance.$transition) frag!.$transition = instance.$transition
         frag!.update(render)
       })
 
@@ -176,10 +180,10 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
 
 function createInnerComp(
   comp: VaporComponent,
-  parent: VaporComponentInstance,
+  parent: VaporComponentInstance & TransitionOptions,
   frag?: DynamicFragment,
 ): VaporComponentInstance {
-  const { rawProps, rawSlots, isSingleRoot, appContext } = parent
+  const { rawProps, rawSlots, isSingleRoot, appContext, $transition } = parent
   const instance = createComponent(
     comp,
     rawProps,
@@ -189,6 +193,9 @@ function createInnerComp(
     appContext,
   )
 
+  // set transition hooks
+  if ($transition) setTransitionHooks(instance, $transition)
+
   // set ref
   // @ts-expect-error
   frag && frag.setRef && frag.setRef(instance)
index 729ebee16fcecf54cf83ac289b531356de3b30a4..0f6d32ae8a53b2feba49b28072422e5d4e803676 100644 (file)
@@ -36,12 +36,20 @@ export interface TransitionOptions {
   $transition?: VaporTransitionHooks
 }
 
-export type TransitionBlock =
-  | (Node & TransitionOptions)
-  | (VaporFragment & TransitionOptions)
-  | (DynamicFragment & TransitionOptions)
+export type TransitionBlock = (
+  | Node
+  | VaporFragment
+  | DynamicFragment
+  | VaporComponentInstance
+) &
+  TransitionOptions
 
-export type Block = TransitionBlock | VaporComponentInstance | Block[]
+export type Block =
+  | Node
+  | VaporFragment
+  | DynamicFragment
+  | VaporComponentInstance
+  | Block[]
 export type BlockFn = (...args: any[]) => Block
 
 export function isBlock(val: NonNullable<unknown>): val is Block {
index 6b1580a9221b81a8b46cb2533df25b4de41b5dfe..131154e2b6cda1bdd46ae91fbd206de96549c263 100644 (file)
@@ -11,6 +11,7 @@ import {
   checkTransitionMode,
   currentInstance,
   getComponentName,
+  isAsyncWrapper,
   isTemplateNode,
   leaveCbKey,
   queuePostFlushCb,
@@ -92,7 +93,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
           if (child) {
             // replace existing transition hooks
             child.$transition!.props = resolvedProps
-            applyTransitionHooks(child, child.$transition!)
+            applyTransitionHooks(child, child.$transition!, undefined, true)
           }
         }
       } else {
@@ -141,7 +142,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
 )
 
 const getTransitionHooksContext = (
-  key: String,
+  key: string,
   props: TransitionProps,
   state: TransitionState,
   instance: GenericComponentInstance,
@@ -210,6 +211,7 @@ export function applyTransitionHooks(
   block: Block,
   hooks: VaporTransitionHooks,
   fallthroughAttrs: boolean = true,
+  isResolved: boolean = false,
 ): VaporTransitionHooks {
   // filter out comment nodes
   if (isArray(block)) {
@@ -222,7 +224,9 @@ export function applyTransitionHooks(
   }
 
   const isFrag = isFragment(block)
-  const child = findTransitionBlock(block, isFrag)
+  const child = isResolved
+    ? (block as TransitionBlock)
+    : findTransitionBlock(block, isFrag)
   if (!child) {
     // set transition hooks on fragment for reusing during it's updating
     if (isFrag) setTransitionHooksOnFragment(block, hooks)
@@ -238,7 +242,7 @@ export function applyTransitionHooks(
     hooks => (resolvedHooks = hooks as VaporTransitionHooks),
   )
   resolvedHooks.delayedLeave = delayedLeave
-  setTransitionHooks(child, resolvedHooks)
+  child.$transition = resolvedHooks
   if (isFrag) setTransitionHooksOnFragment(block, resolvedHooks)
 
   // fallthrough attrs
@@ -266,7 +270,7 @@ export function applyTransitionLeaveHooks(
     state,
     instance,
   )
-  setTransitionHooks(leavingBlock, leavingHooks)
+  leavingBlock.$transition = leavingHooks
 
   const { mode } = props
   if (mode === 'out-in') {
@@ -300,25 +304,25 @@ export function applyTransitionLeaveHooks(
   }
 }
 
-const transitionBlockCache = new WeakMap<Block, TransitionBlock>()
 export function findTransitionBlock(
   block: Block,
   inFragment: boolean = false,
 ): TransitionBlock | undefined {
-  if (transitionBlockCache.has(block)) {
-    return transitionBlockCache.get(block)
-  }
-
   let child: TransitionBlock | undefined
   if (block instanceof Node) {
     // transition can only be applied on Element child
     if (block instanceof Element) child = block
   } else if (isVaporComponent(block)) {
-    // stop searching if encountering nested Transition component
-    if (getComponentName(block.type) === displayName) return undefined
-    child = findTransitionBlock(block.block, inFragment)
-    // use component id as key
-    if (child && child.$key === undefined) child.$key = block.uid
+    // should save hooks on unresolved async wrapper, so that it can be applied after resolved
+    if (isAsyncWrapper(block) && !block.type.__asyncResolved) {
+      child = block
+    } else {
+      // stop searching if encountering nested Transition component
+      if (getComponentName(block.type) === displayName) return undefined
+      child = findTransitionBlock(block.block, inFragment)
+      // use component id as key
+      if (child && child.$key === undefined) child.$key = block.uid
+    }
   } else if (isArray(block)) {
     let hasFound = false
     for (const c of block) {
@@ -369,7 +373,7 @@ export function setTransitionHooksOnFragment(
 }
 
 export function setTransitionHooks(
-  block: TransitionBlock | VaporComponentInstance,
+  block: TransitionBlock,
   hooks: VaporTransitionHooks,
 ): void {
   if (isVaporComponent(block)) {