]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(transition): handle transition on pre-resolved async components (#14314)
authoredison <daiwei521@126.com>
Wed, 14 Jan 2026 12:29:34 +0000 (20:29 +0800)
committerGitHub <noreply@github.com>
Wed, 14 Jan 2026 12:29:34 +0000 (20:29 +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/components/Transition.ts

index f5ed9427c6eeb067ea335776c132438ff639818e..869364dec065e9bdc0c6fd0eb6a036290945d083 100644 (file)
@@ -1179,6 +1179,59 @@ describe('vapor transition', () => {
         '<div class="">vapor compA</div>',
       )
     })
+
+    test('apply transition to pre-resolved async component', async () => {
+      const btnSelector = '.async-resolved > button'
+      const containerSelector = '.async-resolved #container'
+      const hiddenCompSelector = '.async-resolved #hidden-async'
+
+      // Wait for the hidden AsyncCompResolved to resolve and render
+      await waitForInnerHTML(
+        hiddenCompSelector,
+        '<div style="display: none;">vapor compA</div>',
+      )
+
+      expect(await html(containerSelector)).toBe('')
+
+      await click(btnSelector)
+      expect(await html(containerSelector)).toBe(
+        '<div class="v-enter-from v-enter-active">vapor compA</div>',
+      )
+      await waitForInnerHTML(
+        containerSelector,
+        '<div class="v-enter-active v-enter-to">vapor compA</div>',
+      )
+      await waitForInnerHTML(
+        containerSelector,
+        '<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 waitForInnerHTML(
+        containerSelector,
+        '<div class="v-leave-active v-leave-to">vapor compA</div>',
+      )
+      await waitForInnerHTML(containerSelector, '')
+
+      // enter again
+      await click(btnSelector)
+      expect(await html(containerSelector)).toBe(
+        '<div class="v-enter-from v-enter-active">vapor compA</div>',
+      )
+      await waitForInnerHTML(
+        containerSelector,
+        '<div class="v-enter-active v-enter-to">vapor compA</div>',
+      )
+      await waitForInnerHTML(
+        containerSelector,
+        '<div class="">vapor compA</div>',
+      )
+    })
   })
 
   describe('transition with v-show', () => {
index 8e52aeec211e5c405a177bfc7cb1f5487691bae5..904a6c4ae930e5ec299071e5ee2449bcd7a13d06 100644 (file)
@@ -112,6 +112,10 @@ const AsyncComp = defineVaporAsyncComponent(() => {
   return new Promise(resolve => setTimeout(() => resolve(VaporCompA), 50))
 })
 
+const AsyncCompResolved = defineVaporAsyncComponent(() =>
+  Promise.resolve(VaporCompA),
+)
+
 const TrueBranch = defineVaporComponent({
   name: 'TrueBranch',
   setup() {
@@ -649,6 +653,18 @@ const Comp2 = defineVaporComponent({
       </div>
       <button @click="toggle = !toggle">button</button>
     </div>
+    <div class="async-resolved">
+      <!-- Pre-resolve the async component by rendering it hidden -->
+      <div id="hidden-async">
+        <AsyncCompResolved v-show="false" />
+      </div>
+      <div id="container">
+        <transition>
+          <AsyncCompResolved v-if="!toggle"></AsyncCompResolved>
+        </transition>
+      </div>
+      <button @click="toggle = !toggle">button</button>
+    </div>
     <!-- async component end -->
 
     <!-- with teleport -->
index 0493e2ea1f7b0d62f3d0828e94537cb6270ff276..fccc28bcb04d29b2aaefeeef37b1d2619177eaab 100644 (file)
@@ -28,7 +28,6 @@ import {
 import { invokeArrayFns } from '@vue/shared'
 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>(
@@ -167,7 +166,6 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
           render = () => createComponent(loadingComponent)
         }
 
-        if (instance.$transition) frag!.$transition = instance.$transition
         frag.update(render)
         // Manually trigger cacheBlock for KeepAlive
         if (frag.keepAliveCtx) frag.keepAliveCtx.cacheBlock()
@@ -183,7 +181,7 @@ function createInnerComp(
   parent: VaporComponentInstance & TransitionOptions,
   frag?: DynamicFragment,
 ): VaporComponentInstance {
-  const { rawProps, rawSlots, appContext, $transition } = parent
+  const { rawProps, rawSlots, appContext } = parent
   const instance = createComponent(
     comp,
     rawProps,
@@ -195,9 +193,6 @@ function createInnerComp(
     appContext,
   )
 
-  // set transition hooks
-  if ($transition) setTransitionHooks(instance, $transition)
-
   // set ref
   frag && frag.setAsyncRef && frag.setAsyncRef(instance)
 
index 62f991c3b46be001bdb2e7631b6513df8bc9d608..79c3ed942e5c62afb4e8d29474b2e18c97c099f3 100644 (file)
@@ -32,7 +32,11 @@ import {
 } from '../component'
 import { isArray } from '@vue/shared'
 import { renderEffect } from '../renderEffect'
-import { type VaporFragment, isFragment } from '../fragment'
+import {
+  type DynamicFragment,
+  type VaporFragment,
+  isFragment,
+} from '../fragment'
 import {
   currentHydrationNode,
   isHydrating,
@@ -288,9 +292,16 @@ export function findTransitionBlock(
     // transition can only be applied on Element child
     if (block instanceof Element) child = block
   } else if (isVaporComponent(block)) {
-    // should save hooks on unresolved async wrapper, so that it can be applied after resolved
-    if (isAsyncWrapper(block) && !block.type.__asyncResolved) {
-      child = block
+    if (isAsyncWrapper(block)) {
+      // for unresolved async wrapper, set transition hooks on inner fragment
+      if (!block.type.__asyncResolved) {
+        onFragment && onFragment(block.block! as DynamicFragment)
+      } else {
+        child = findTransitionBlock(
+          (block.block! as DynamicFragment).nodes,
+          onFragment,
+        )
+      }
     } else {
       // stop searching if encountering nested Transition component
       if (getComponentName(block.type) === displayName) return undefined