export const PORTAL = Symbol(__DEV__ ? `Portal` : ``)
export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``)
export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``)
+export const TRANSITION = Symbol(__DEV__ ? `Transition` : ``)
+export const BASE_TRANSITION = Symbol(__DEV__ ? `BaseTransition` : ``)
export const OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ``)
export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
[PORTAL]: `Portal`,
[SUSPENSE]: `Suspense`,
[KEEP_ALIVE]: `KeepAlive`,
+ [TRANSITION]: `Transition`,
+ [BASE_TRANSITION]: `BaseTransition`,
[OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`,
[CREATE_VNODE]: `createVNode`,
createObjectExpression,
Property
} from '../ast'
-import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared'
+import { PatchFlags, PatchFlagNames, isSymbol, hyphenate } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import {
CREATE_VNODE,
TO_HANDLERS,
PORTAL,
SUSPENSE,
- KEEP_ALIVE
+ KEEP_ALIVE,
+ TRANSITION
} from '../runtimeHelpers'
import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils'
import { buildSlots } from './vSlot'
// import, which should be used instead of a resolveDirective call.
const directiveImportMap = new WeakMap<DirectiveNode, symbol>()
+const isBuiltInType = (tag: string, expected: string): boolean =>
+ tag === expected || tag === hyphenate(expected)
+
// generate a JavaScript AST for this element's codegen
export const transformElement: NodeTransform = (node, context) => {
if (
// processed and merged.
return function postTransformElement() {
const { tag, tagType, props } = node
- const isPortal = tag === 'portal' || tag === 'Portal'
- const isSuspense = tag === 'suspense' || tag === 'Suspense'
- const isKeepAlive = tag === 'keep-alive' || tag === 'KeepAlive'
+ const isPortal = isBuiltInType(tag, 'Portal')
+ const isSuspense = isBuiltInType(tag, 'Suspense')
+ const isKeepAlive = isBuiltInType(tag, 'KeepAlive')
+ const isTransition = isBuiltInType(tag, 'Transition')
const isComponent = tagType === ElementTypes.COMPONENT
let hasProps = props.length > 0
nodeType = context.helper(SUSPENSE)
} else if (isKeepAlive) {
nodeType = context.helper(KEEP_ALIVE)
+ } else if (isTransition) {
+ nodeType = context.helper(TRANSITION)
} else if (isComponent) {
// user component w/ resolve
context.helper(RESOLVE_COMPONENT)
import { toRaw } from '@vue/reactivity'
import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
import { ShapeFlags } from '../shapeFlags'
+import { onBeforeUnmount, onMounted } from '../apiLifecycle'
export interface BaseTransitionProps {
mode?: 'in-out' | 'out-in' | 'default'
persisted?: boolean
// Hooks. Using camel casef for easier usage in render functions & JSX.
- // In templates these will be written as @before-enter="xxx"
- // The compiler has special handling to convert them into the proper cases.
- // enter
+ // In templates these can be written as @before-enter="xxx" as prop names
+ // are camelized
onBeforeEnter?: (el: any) => void
onEnter?: (el: any, done: () => void) => void
onAfterEnter?: (el: any) => void
args?: any[]
) => void
-interface PendingCallbacks {
- enter?: (cancelled?: boolean) => void
- leave?: (cancelled?: boolean) => void
+interface TransitionState {
+ isMounted: boolean
+ isLeaving: boolean
+ isUnmounting: boolean
+ pendingEnter?: (cancelled?: boolean) => void
+ pendingLeave?: (cancelled?: boolean) => void
}
const BaseTransitionImpl = {
name: `BaseTransition`,
setup(props: BaseTransitionProps, { slots }: SetupContext) {
const instance = getCurrentInstance()!
- const pendingCallbacks: PendingCallbacks = {}
- let isLeaving = false
+ const state: TransitionState = {
+ isMounted: false,
+ isLeaving: false,
+ isUnmounting: false
+ }
+ onMounted(() => {
+ state.isMounted = true
+ })
+ onBeforeUnmount(() => {
+ state.isUnmounting = true
+ })
const callTransitionHook: TransitionHookCaller = (hook, args) => {
hook &&
// at this point children has a guaranteed length of 1.
const child = children[0]
- if (isLeaving) {
+ if (state.isLeaving) {
return placeholder(child)
}
let delayedLeave: (() => void) | undefined
const performDelayedLeave = () => delayedLeave && delayedLeave()
+
const transitionHooks = (child.transition = resolveTransitionHooks(
rawProps,
+ state,
callTransitionHook,
- instance.isMounted,
- pendingCallbacks,
performDelayedLeave
))
updateHOCTransitionData(oldChild, transitionHooks)
// switching between different views
if (mode === 'out-in') {
- isLeaving = true
+ state.isLeaving = true
// return placeholder node and queue update when leave finishes
transitionHooks.afterLeave = () => {
- isLeaving = false
+ state.isLeaving = false
instance.update()
}
return placeholder(child)
onAfterLeave,
onLeaveCancelled
}: BaseTransitionProps,
+ state: TransitionState,
callHook: TransitionHookCaller,
- isMounted: boolean,
- pendingCallbacks: PendingCallbacks,
performDelayedLeave: () => void
): TransitionHooks {
return {
persisted,
beforeEnter(el) {
- if (!isMounted && !appear) {
- return
+ if (state.pendingLeave) {
+ state.pendingLeave(true /* cancelled */)
}
- if (pendingCallbacks.leave) {
- pendingCallbacks.leave(true /* cancelled */)
+ if (!appear && !state.isMounted) {
+ return
}
callHook(onBeforeEnter, [el])
},
enter(el) {
- if (!isMounted && !appear) {
+ if (!appear && !state.isMounted) {
return
}
let called = false
- const afterEnter = (pendingCallbacks.enter = (cancelled?) => {
+ const afterEnter = (state.pendingEnter = (cancelled?) => {
if (called) return
called = true
if (cancelled) {
callHook(onAfterEnter, [el])
performDelayedLeave()
}
- pendingCallbacks.enter = undefined
+ state.pendingEnter = undefined
})
if (onEnter) {
onEnter(el, afterEnter)
},
leave(el, remove) {
- if (pendingCallbacks.enter) {
- pendingCallbacks.enter(true /* cancelled */)
+ if (state.pendingEnter) {
+ state.pendingEnter(true /* cancelled */)
+ }
+ if (state.isUnmounting) {
+ return remove()
}
callHook(onBeforeLeave, [el])
let called = false
- const afterLeave = (pendingCallbacks.leave = (cancelled?) => {
+ const afterLeave = (state.pendingLeave = (cancelled?) => {
if (called) return
called = true
remove()
} else {
callHook(onAfterLeave, [el])
}
- pendingCallbacks.leave = undefined
+ state.pendingLeave = undefined
})
if (onLeave) {
onLeave(el, afterLeave)
export interface TransitionProps extends BaseTransitionProps {
name?: string
type?: typeof TRANSITION | typeof ANIMATION
+ css?: boolean
duration?: number | { enter: number; leave: number }
// custom transition classes
enterFromClass?: string
...(BaseTransition as any).props,
name: String,
type: String,
+ // Cannot use Boolean otherwise it will be force casted to false when
+ // omitted
+ css: null,
+ duration: Object,
enterFromClass: String,
enterActiveClass: String,
enterToClass: String,
appearToClass: String,
leaveFromClass: String,
leaveActiveClass: String,
- leaveToClass: String,
- duration: Object
+ leaveToClass: String
}
}
function resolveTransitionProps({
name = 'v',
type,
+ css = true,
duration,
enterFromClass = `${name}-enter-from`,
enterActiveClass = `${name}-enter-active`,
leaveToClass = `${name}-leave-to`,
...baseProps
}: TransitionProps): BaseTransitionProps {
+ if (!css) {
+ return baseProps
+ }
+
const instance = getCurrentInstance()!
const durations = normalizeDuration(duration)
const enterDuration = durations && durations[0]