Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,44 @@ describe('vapor transition-group', () => {
expect(calls).toContain('afterEnter')
})

test(
'reusable transition group',
async () => {
const btnSelector = '.reusable-transition-group > button'
const containerSelector = '.reusable-transition-group > div'

expect(await html(containerSelector)).toBe(
`<div class="test">a</div>` +
`<div class="test">b</div>` +
`<div class="test">c</div>`,
)

expect(
(await transitionStart(btnSelector, containerSelector)).innerHTML,
).toBe(
`<div class="test group-enter-from group-enter-active">d</div>` +
`<div class="test">b</div>` +
`<div class="test group-move" style="">a</div>` +
`<div class="test group-leave-from group-leave-active group-move" style="">c</div>`,
)

await nextFrame()
expect(await html(containerSelector)).toBe(
`<div class="test group-enter-active group-enter-to">d</div>` +
`<div class="test">b</div>` +
`<div class="test group-move" style="">a</div>` +
`<div class="test group-leave-active group-move group-leave-to" style="">c</div>`,
)
await transitionFinish(duration * 2)
expect(await html(containerSelector)).toBe(
`<div class="test">d</div>` +
`<div class="test">b</div>` +
`<div class="test" style="">a</div>`,
)
},
E2E_TIMEOUT,
)

test('interop: render vdom component', async () => {
const btnSelector = '.interop > button'
const containerSelector = '.interop > div'
Expand Down
9 changes: 9 additions & 0 deletions packages-private/vapor-e2e-test/transition-group/App.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup vapor>
import { ref } from 'vue'
import VdomComp from './components/VdomComp.vue'
import MyTransitionGroup from './components/MyTransitionGroup.vue'
const items = ref(['a', 'b', 'c'])
const enterClick = () => items.value.push('d', 'e')
Expand Down Expand Up @@ -108,6 +109,14 @@ const interopClick = () => (items.value = ['b', 'c', 'd'])
</transition-group>
</div>
</div>
<div class="reusable-transition-group">
<button @click="moveClick">reusable button</button>
<div>
<MyTransitionGroup name="group">
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
</MyTransitionGroup>
</div>
</div>
<div class="interop">
<button @click="interopClick">interop button</button>
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script setup vapor></script>

<template>
<TransitionGroup>
<slot />
</TransitionGroup>
</template>
17 changes: 2 additions & 15 deletions packages/compiler-vapor/src/generators/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,7 @@ import { genEventHandler } from './event'
import { genDirectiveModifiers, genDirectivesForElement } from './directive'
import { genBlock } from './block'
import { genModelHandler } from './vModel'
import {
isBuiltInComponent,
isKeepAliveTag,
isTeleportTag,
isTransitionGroupTag,
} from '../utils'
import { isBuiltInComponent } from '../utils'

export function genCreateComponent(
operation: CreateComponentIRNode,
Expand Down Expand Up @@ -465,15 +460,7 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
]
}

if (
node.type === NodeTypes.ELEMENT &&
// Not a real component
!isTeleportTag(node.tag) &&
// Needs to determine whether to activate/deactivate based on instance.parent being KeepAlive
!isKeepAliveTag(node.tag) &&
// Slot updates need to trigger TransitionGroup's onBeforeUpdate/onUpdated hook
!isTransitionGroupTag(node.tag)
) {
if (node.type === NodeTypes.ELEMENT) {
// wrap with withVaporCtx to ensure correct currentInstance inside slot
blockFn = [`${context.helper('withVaporCtx')}(`, ...blockFn, `)`]
}
Expand Down
7 changes: 7 additions & 0 deletions packages/runtime-vapor/src/apiCreateFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
isLastInsertion,
resetInsertionState,
} from './insertionState'
import { triggerTransitionGroupUpdate } from './components/TransitionGroup'

class ForBlock extends VaporFragment {
scope: EffectScope | undefined
Expand Down Expand Up @@ -130,6 +131,12 @@ export const createFor = (
newBlocks = new Array(newLength)
let isFallback = false

// trigger TransitionGroup update hooks
const transitionHooks = frag.$transition
if (transitionHooks && transitionHooks.group) {
triggerTransitionGroupUpdate(transitionHooks)
}

const prevSub = setActiveSub()

if (!isMounted) {
Expand Down
3 changes: 3 additions & 0 deletions packages/runtime-vapor/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export interface VaporTransitionHooks extends TransitionHooks {
// mark transition hooks as disabled so that it skips during
// inserting
disabled?: boolean
// mark transition hooks as group so that it triggers TransitionGroup update hooks
// in vFor renderList function
group?: boolean
}

export interface TransitionOptions {
Expand Down
6 changes: 5 additions & 1 deletion packages/runtime-vapor/src/components/Transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export function applyTransitionHooks(
return hooks
}

const { props, instance, state, delayedLeave } = hooks
const { props, instance, state, delayedLeave, group } = hooks
let resolvedHooks = resolveTransitionHooks(
child,
props,
Expand All @@ -242,6 +242,7 @@ export function applyTransitionHooks(
hooks => (resolvedHooks = hooks as VaporTransitionHooks),
)
resolvedHooks.delayedLeave = delayedLeave
resolvedHooks.group = group
child.$transition = resolvedHooks
if (isFrag) setTransitionHooksOnFragment(block, resolvedHooks)

Expand Down Expand Up @@ -365,6 +366,9 @@ export function setTransitionHooksOnFragment(
): void {
if (isFragment(block)) {
block.$transition = hooks
if (block.nodes && isFragment(block.nodes)) {
setTransitionHooksOnFragment(block.nodes, hooks)
}
} else if (isArray(block)) {
for (let i = 0; i < block.length; i++) {
setTransitionHooksOnFragment(block[i], hooks)
Expand Down
32 changes: 29 additions & 3 deletions packages/runtime-vapor/src/components/TransitionGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import {
hasCSSTransform,
onBeforeUpdate,
onUpdated,
queuePostFlushCb,
resolveTransitionProps,
useTransitionState,
warn,
} from '@vue/runtime-dom'
import { extend, isArray } from '@vue/shared'
import { extend, invokeArrayFns, isArray } from '@vue/shared'
import {
type Block,
type TransitionBlock,
Expand Down Expand Up @@ -126,17 +127,22 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({
props: cssTransitionProps,
state,
instance,
group: true,
} as VaporTransitionHooks)

children = getTransitionBlocks(slottedBlock)
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (isValidTransitionBlock(child)) {
if (child.$key != null) {
setTransitionHooks(
const hooks = resolveTransitionHooks(
child,
resolveTransitionHooks(child, cssTransitionProps, state, instance!),
cssTransitionProps,
state,
instance!,
)
hooks.group = true
setTransitionHooks(child, hooks)
} else if (__DEV__ && child.$key == null) {
warn(`<transition-group> children must be keyed`)
}
Expand Down Expand Up @@ -221,3 +227,23 @@ function getFirstConnectedChild(
if (el.isConnected) return el
}
}

/**
* The implementation of TransitionGroup relies on the onBeforeUpdate and onUpdated hooks.
* However, when the slot content of TransitionGroup updates, it does not trigger the
* onBeforeUpdate and onUpdated hooks. Therefore, it is necessary to manually trigger
* the TransitionGroup update hooks to ensure its proper work.
*/
export function triggerTransitionGroupUpdate(
transition: VaporTransitionHooks,
): void {
const { instance } = transition
if (!instance.isUpdating) {
instance.isUpdating = true
if (instance.bu) invokeArrayFns(instance.bu)
queuePostFlushCb(() => {
instance.isUpdating = false
if (instance.u) invokeArrayFns(instance.u)
})
}
}
Loading