Skip to content

Commit 171f3f5

Browse files
authored
fix(transition-group): support reusable transition group (#14077)
1 parent abe8fc2 commit 171f3f5

File tree

8 files changed

+100
-19
lines changed

8 files changed

+100
-19
lines changed

packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,44 @@ describe('vapor transition-group', () => {
369369
expect(calls).toContain('afterEnter')
370370
})
371371

372+
test(
373+
'reusable transition group',
374+
async () => {
375+
const btnSelector = '.reusable-transition-group > button'
376+
const containerSelector = '.reusable-transition-group > div'
377+
378+
expect(await html(containerSelector)).toBe(
379+
`<div class="test">a</div>` +
380+
`<div class="test">b</div>` +
381+
`<div class="test">c</div>`,
382+
)
383+
384+
expect(
385+
(await transitionStart(btnSelector, containerSelector)).innerHTML,
386+
).toBe(
387+
`<div class="test group-enter-from group-enter-active">d</div>` +
388+
`<div class="test">b</div>` +
389+
`<div class="test group-move" style="">a</div>` +
390+
`<div class="test group-leave-from group-leave-active group-move" style="">c</div>`,
391+
)
392+
393+
await nextFrame()
394+
expect(await html(containerSelector)).toBe(
395+
`<div class="test group-enter-active group-enter-to">d</div>` +
396+
`<div class="test">b</div>` +
397+
`<div class="test group-move" style="">a</div>` +
398+
`<div class="test group-leave-active group-move group-leave-to" style="">c</div>`,
399+
)
400+
await transitionFinish(duration * 2)
401+
expect(await html(containerSelector)).toBe(
402+
`<div class="test">d</div>` +
403+
`<div class="test">b</div>` +
404+
`<div class="test" style="">a</div>`,
405+
)
406+
},
407+
E2E_TIMEOUT,
408+
)
409+
372410
test('interop: render vdom component', async () => {
373411
const btnSelector = '.interop > button'
374412
const containerSelector = '.interop > div'

packages-private/vapor-e2e-test/transition-group/App.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup vapor>
22
import { ref } from 'vue'
33
import VdomComp from './components/VdomComp.vue'
4+
import MyTransitionGroup from './components/MyTransitionGroup.vue'
45
56
const items = ref(['a', 'b', 'c'])
67
const enterClick = () => items.value.push('d', 'e')
@@ -108,6 +109,14 @@ const interopClick = () => (items.value = ['b', 'c', 'd'])
108109
</transition-group>
109110
</div>
110111
</div>
112+
<div class="reusable-transition-group">
113+
<button @click="moveClick">reusable button</button>
114+
<div>
115+
<MyTransitionGroup name="group">
116+
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
117+
</MyTransitionGroup>
118+
</div>
119+
</div>
111120
<div class="interop">
112121
<button @click="interopClick">interop button</button>
113122
<div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script setup vapor></script>
2+
3+
<template>
4+
<TransitionGroup>
5+
<slot />
6+
</TransitionGroup>
7+
</template>

packages/compiler-vapor/src/generators/component.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,7 @@ import { genEventHandler } from './event'
4040
import { genDirectiveModifiers, genDirectivesForElement } from './directive'
4141
import { genBlock } from './block'
4242
import { genModelHandler } from './vModel'
43-
import {
44-
isBuiltInComponent,
45-
isKeepAliveTag,
46-
isTeleportTag,
47-
isTransitionGroupTag,
48-
} from '../utils'
43+
import { isBuiltInComponent } from '../utils'
4944

5045
export function genCreateComponent(
5146
operation: CreateComponentIRNode,
@@ -465,15 +460,7 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
465460
]
466461
}
467462

468-
if (
469-
node.type === NodeTypes.ELEMENT &&
470-
// Not a real component
471-
!isTeleportTag(node.tag) &&
472-
// Needs to determine whether to activate/deactivate based on instance.parent being KeepAlive
473-
!isKeepAliveTag(node.tag) &&
474-
// Slot updates need to trigger TransitionGroup's onBeforeUpdate/onUpdated hook
475-
!isTransitionGroupTag(node.tag)
476-
) {
463+
if (node.type === NodeTypes.ELEMENT) {
477464
// wrap with withVaporCtx to ensure correct currentInstance inside slot
478465
blockFn = [`${context.helper('withVaporCtx')}(`, ...blockFn, `)`]
479466
}

packages/runtime-vapor/src/apiCreateFor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
isLastInsertion,
4040
resetInsertionState,
4141
} from './insertionState'
42+
import { triggerTransitionGroupUpdate } from './components/TransitionGroup'
4243

4344
class ForBlock extends VaporFragment {
4445
scope: EffectScope | undefined
@@ -130,6 +131,12 @@ export const createFor = (
130131
newBlocks = new Array(newLength)
131132
let isFallback = false
132133

134+
// trigger TransitionGroup update hooks
135+
const transitionHooks = frag.$transition
136+
if (transitionHooks && transitionHooks.group) {
137+
triggerTransitionGroupUpdate(transitionHooks)
138+
}
139+
133140
const prevSub = setActiveSub()
134141

135142
if (!isMounted) {

packages/runtime-vapor/src/block.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export interface VaporTransitionHooks extends TransitionHooks {
2929
// mark transition hooks as disabled so that it skips during
3030
// inserting
3131
disabled?: boolean
32+
// mark transition hooks as group so that it triggers TransitionGroup update hooks
33+
// in vFor renderList function
34+
group?: boolean
3235
}
3336

3437
export interface TransitionOptions {

packages/runtime-vapor/src/components/Transition.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export function applyTransitionHooks(
233233
return hooks
234234
}
235235

236-
const { props, instance, state, delayedLeave } = hooks
236+
const { props, instance, state, delayedLeave, group } = hooks
237237
let resolvedHooks = resolveTransitionHooks(
238238
child,
239239
props,
@@ -242,6 +242,7 @@ export function applyTransitionHooks(
242242
hooks => (resolvedHooks = hooks as VaporTransitionHooks),
243243
)
244244
resolvedHooks.delayedLeave = delayedLeave
245+
resolvedHooks.group = group
245246
child.$transition = resolvedHooks
246247
if (isFrag) setTransitionHooksOnFragment(block, resolvedHooks)
247248

@@ -365,6 +366,9 @@ export function setTransitionHooksOnFragment(
365366
): void {
366367
if (isFragment(block)) {
367368
block.$transition = hooks
369+
if (block.nodes && isFragment(block.nodes)) {
370+
setTransitionHooksOnFragment(block.nodes, hooks)
371+
}
368372
} else if (isArray(block)) {
369373
for (let i = 0; i < block.length; i++) {
370374
setTransitionHooksOnFragment(block[i], hooks)

packages/runtime-vapor/src/components/TransitionGroup.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import {
1010
hasCSSTransform,
1111
onBeforeUpdate,
1212
onUpdated,
13+
queuePostFlushCb,
1314
resolveTransitionProps,
1415
useTransitionState,
1516
warn,
1617
} from '@vue/runtime-dom'
17-
import { extend, isArray } from '@vue/shared'
18+
import { extend, invokeArrayFns, isArray } from '@vue/shared'
1819
import {
1920
type Block,
2021
type TransitionBlock,
@@ -126,17 +127,22 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({
126127
props: cssTransitionProps,
127128
state,
128129
instance,
130+
group: true,
129131
} as VaporTransitionHooks)
130132

131133
children = getTransitionBlocks(slottedBlock)
132134
for (let i = 0; i < children.length; i++) {
133135
const child = children[i]
134136
if (isValidTransitionBlock(child)) {
135137
if (child.$key != null) {
136-
setTransitionHooks(
138+
const hooks = resolveTransitionHooks(
137139
child,
138-
resolveTransitionHooks(child, cssTransitionProps, state, instance!),
140+
cssTransitionProps,
141+
state,
142+
instance!,
139143
)
144+
hooks.group = true
145+
setTransitionHooks(child, hooks)
140146
} else if (__DEV__ && child.$key == null) {
141147
warn(`<transition-group> children must be keyed`)
142148
}
@@ -221,3 +227,23 @@ function getFirstConnectedChild(
221227
if (el.isConnected) return el
222228
}
223229
}
230+
231+
/**
232+
* The implementation of TransitionGroup relies on the onBeforeUpdate and onUpdated hooks.
233+
* However, when the slot content of TransitionGroup updates, it does not trigger the
234+
* onBeforeUpdate and onUpdated hooks. Therefore, it is necessary to manually trigger
235+
* the TransitionGroup update hooks to ensure its proper work.
236+
*/
237+
export function triggerTransitionGroupUpdate(
238+
transition: VaporTransitionHooks,
239+
): void {
240+
const { instance } = transition
241+
if (!instance.isUpdating) {
242+
instance.isUpdating = true
243+
if (instance.bu) invokeArrayFns(instance.bu)
244+
queuePostFlushCb(() => {
245+
instance.isUpdating = false
246+
if (instance.u) invokeArrayFns(instance.u)
247+
})
248+
}
249+
}

0 commit comments

Comments
 (0)