Browse Source

fix(transition): add key for transition if-branches (#14374)

close #14368
edison 2 tháng trước cách đây
mục cha
commit
e08308e043

+ 78 - 0
packages-private/vapor-e2e-test/__tests__/transition.spec.ts

@@ -87,6 +87,84 @@ describe('vapor transition', () => {
       E2E_TIMEOUT,
     )
 
+    test(
+      'if/else-if/else chain transition',
+      async () => {
+        const btnSelector = '.if-else-chain > button'
+        const containerSelector = '.if-else-chain > div'
+        const childSelector = `${containerSelector} > div`
+
+        expect(await html(containerSelector)).toBe('<div class="test">0</div>')
+
+        expect(
+          (await transitionStart(btnSelector, childSelector)).outerHTML,
+        ).toBe(`<div class="test v-leave-from v-leave-active">0</div>`)
+
+        await waitForInnerHTML(containerSelector, '<div class="test">1</div>')
+
+        expect(
+          (await transitionStart(btnSelector, childSelector)).outerHTML,
+        ).toBe(`<div class="test v-leave-from v-leave-active">1</div>`)
+
+        await waitForInnerHTML(containerSelector, '<div class="test">2</div>')
+      },
+      E2E_TIMEOUT,
+    )
+
+    test(
+      'if/else-if/else chain transition (out-in mode)',
+      async () => {
+        const btnSelector = '.if-else-chain-out-in > button'
+        const containerSelector = '.if-else-chain-out-in > div'
+        const childSelector = `${containerSelector} > div`
+
+        expect(await html(containerSelector)).toBe('<div class="test">0</div>')
+
+        expect(
+          (await transitionStart(btnSelector, childSelector)).outerHTML,
+        ).toBe(`<div class="test v-leave-from v-leave-active">0</div>`)
+
+        await waitForInnerHTML(
+          containerSelector,
+          '<div class="test v-leave-active v-leave-to">0</div>',
+        )
+
+        await waitForInnerHTML(
+          containerSelector,
+          '<div class="test v-enter-from v-enter-active">1</div>',
+        )
+
+        await waitForInnerHTML(
+          containerSelector,
+          '<div class="test v-enter-active v-enter-to">1</div>',
+        )
+
+        await waitForInnerHTML(containerSelector, '<div class="test">1</div>')
+
+        expect(
+          (await transitionStart(btnSelector, childSelector)).outerHTML,
+        ).toBe(`<div class="test v-leave-from v-leave-active">1</div>`)
+
+        await waitForInnerHTML(
+          containerSelector,
+          '<div class="test v-leave-active v-leave-to">1</div>',
+        )
+
+        await waitForInnerHTML(
+          containerSelector,
+          '<div class="test v-enter-from v-enter-active">2</div>',
+        )
+
+        await waitForInnerHTML(
+          containerSelector,
+          '<div class="test v-enter-active v-enter-to">2</div>',
+        )
+
+        await waitForInnerHTML(containerSelector, '<div class="test">2</div>')
+      },
+      E2E_TIMEOUT,
+    )
+
     test(
       'named transition',
       async () => {

+ 20 - 0
packages-private/vapor-e2e-test/transition/App.vue

@@ -289,6 +289,26 @@ const Comp2 = defineVaporComponent({
       </div>
       <button @click="toggle = !toggle">basic toggle</button>
     </div>
+    <div class="if-else-chain">
+      <div>
+        <transition>
+          <div v-if="count === 0" class="test">0</div>
+          <div v-else-if="count === 1" class="test">1</div>
+          <div v-else class="test">2</div>
+        </transition>
+      </div>
+      <button @click="count++">inc</button>
+    </div>
+    <div class="if-else-chain-out-in">
+      <div>
+        <transition mode="out-in">
+          <div v-if="count === 0" class="test">0</div>
+          <div v-else-if="count === 1" class="test">1</div>
+          <div v-else class="test">2</div>
+        </transition>
+      </div>
+      <button @click="count++">inc</button>
+    </div>
     <div class="if-named">
       <div>
         <transition name="test">

+ 1 - 1
packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap

@@ -58,7 +58,7 @@ export function render(_ctx) {
           return n13
         })
         return n14
-      }))
+      }, null, 1), null, 0)
       return [n0, n3, n7]
     }
   }, true)

+ 3 - 1
packages/compiler-vapor/src/generators/if.ts

@@ -10,7 +10,7 @@ export function genIf(
   isNested = false,
 ): CodeFragment[] {
   const { helper } = context
-  const { condition, positive, negative, once } = oper
+  const { condition, positive, negative, once, index } = oper
   const [frag, push] = buildCodeFragment()
 
   const conditionExpr: CodeFragment[] = [
@@ -38,6 +38,8 @@ export function genIf(
       positiveArg,
       negativeArg,
       once && 'true',
+      // index is only used when the branch can change in Transition
+      index !== undefined && negative && String(index),
     ),
   )
 

+ 1 - 0
packages/compiler-vapor/src/ir/index.ts

@@ -76,6 +76,7 @@ export interface IfIRNode extends BaseIRNode {
   positive: BlockIRNode
   negative?: BlockIRNode | IfIRNode
   once?: boolean
+  index?: number
   parent?: number
   anchor?: number
   logicalIndex?: number

+ 5 - 0
packages/compiler-vapor/src/transform.ts

@@ -111,6 +111,7 @@ export class TransformContext<T extends AllNode = AllNode> {
 
   private globalId = 0
   private nextIdMap: Map<number, number> | null = null
+  private ifIndex = 0
 
   constructor(
     public ir: RootIRNode,
@@ -175,6 +176,10 @@ export class TransformContext<T extends AllNode = AllNode> {
     return (this.dynamic.id = this.increaseId())
   }
 
+  nextIfIndex(): number {
+    return this.ifIndex++
+  }
+
   pushTemplate(content: string): number {
     const existingIndex = this.ir.templateIndexMap.get(content)
     if (existingIndex !== undefined) {

+ 3 - 1
packages/compiler-vapor/src/transforms/vIf.ts

@@ -18,7 +18,7 @@ import {
 import { extend } from '@vue/shared'
 import { newBlock, wrapTemplate } from './utils'
 import { getSiblingIf } from './transformComment'
-import { isStaticExpression } from '../utils'
+import { isInTransition, isStaticExpression } from '../utils'
 
 export const transformVIf: NodeTransform = createStructuralDirectiveTransform(
   ['if', 'else', 'else-if'],
@@ -51,6 +51,7 @@ export function processIf(
         id,
         condition: dir.exp!,
         positive: branch,
+        index: isInTransition(context) ? context.root.nextIfIndex() : undefined,
         once:
           context.inVOnce ||
           isStaticExpression(dir.exp!, context.options.bindingMetadata),
@@ -118,6 +119,7 @@ export function processIf(
         id: -1,
         condition: dir.exp!,
         positive: branch,
+        index: isInTransition(context) ? context.root.nextIfIndex() : undefined,
         once:
           context.inVOnce ||
           isStaticExpression(dir.exp!, context.options.bindingMetadata),

+ 13 - 2
packages/runtime-vapor/src/apiCreateIf.ts

@@ -15,6 +15,7 @@ export function createIf(
   b1: BlockFn,
   b2?: BlockFn,
   once?: boolean,
+  index?: number,
 ): Block {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
@@ -29,9 +30,19 @@ export function createIf(
         ? b2()
         : [__DEV__ ? createComment('if') : createTextNode()]
   } else {
+    // DynamicFragment should be keyed for correct transition behavior
+    const keyed = index != null
     frag =
-      isHydrating || __DEV__ ? new DynamicFragment('if') : new DynamicFragment()
-    renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2))
+      isHydrating || __DEV__
+        ? new DynamicFragment('if', keyed)
+        : new DynamicFragment(undefined, keyed)
+    renderEffect(() => {
+      const ok = condition()
+      ;(frag as DynamicFragment).update(
+        ok ? b1 : b2,
+        keyed ? `${index}${ok ? 0 : 1}` : undefined,
+      )
+    })
   }
 
   if (!isHydrating) {