feat(transition): support in templates

This commit is contained in:
Evan You 2019-11-24 18:37:59 -05:00
parent a834807942
commit 1765985ec2
4 changed files with 67 additions and 33 deletions

View File

@ -2,6 +2,8 @@ export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``)
export const PORTAL = Symbol(__DEV__ ? `Portal` : ``) export const PORTAL = Symbol(__DEV__ ? `Portal` : ``)
export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``) export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``)
export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``) 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 OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ``)
export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``) export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``) export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
@ -30,6 +32,8 @@ export const helperNameMap: any = {
[PORTAL]: `Portal`, [PORTAL]: `Portal`,
[SUSPENSE]: `Suspense`, [SUSPENSE]: `Suspense`,
[KEEP_ALIVE]: `KeepAlive`, [KEEP_ALIVE]: `KeepAlive`,
[TRANSITION]: `Transition`,
[BASE_TRANSITION]: `BaseTransition`,
[OPEN_BLOCK]: `openBlock`, [OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`, [CREATE_BLOCK]: `createBlock`,
[CREATE_VNODE]: `createVNode`, [CREATE_VNODE]: `createVNode`,

View File

@ -15,7 +15,7 @@ import {
createObjectExpression, createObjectExpression,
Property Property
} from '../ast' } from '../ast'
import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared' import { PatchFlags, PatchFlagNames, isSymbol, hyphenate } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
CREATE_VNODE, CREATE_VNODE,
@ -27,7 +27,8 @@ import {
TO_HANDLERS, TO_HANDLERS,
PORTAL, PORTAL,
SUSPENSE, SUSPENSE,
KEEP_ALIVE KEEP_ALIVE,
TRANSITION
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils' import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils'
import { buildSlots } from './vSlot' import { buildSlots } from './vSlot'
@ -37,6 +38,9 @@ import { isStaticNode } from './hoistStatic'
// import, which should be used instead of a resolveDirective call. // import, which should be used instead of a resolveDirective call.
const directiveImportMap = new WeakMap<DirectiveNode, symbol>() 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 // generate a JavaScript AST for this element's codegen
export const transformElement: NodeTransform = (node, context) => { export const transformElement: NodeTransform = (node, context) => {
if ( if (
@ -53,9 +57,10 @@ export const transformElement: NodeTransform = (node, context) => {
// processed and merged. // processed and merged.
return function postTransformElement() { return function postTransformElement() {
const { tag, tagType, props } = node const { tag, tagType, props } = node
const isPortal = tag === 'portal' || tag === 'Portal' const isPortal = isBuiltInType(tag, 'Portal')
const isSuspense = tag === 'suspense' || tag === 'Suspense' const isSuspense = isBuiltInType(tag, 'Suspense')
const isKeepAlive = tag === 'keep-alive' || tag === 'KeepAlive' const isKeepAlive = isBuiltInType(tag, 'KeepAlive')
const isTransition = isBuiltInType(tag, 'Transition')
const isComponent = tagType === ElementTypes.COMPONENT const isComponent = tagType === ElementTypes.COMPONENT
let hasProps = props.length > 0 let hasProps = props.length > 0
@ -96,6 +101,8 @@ export const transformElement: NodeTransform = (node, context) => {
nodeType = context.helper(SUSPENSE) nodeType = context.helper(SUSPENSE)
} else if (isKeepAlive) { } else if (isKeepAlive) {
nodeType = context.helper(KEEP_ALIVE) nodeType = context.helper(KEEP_ALIVE)
} else if (isTransition) {
nodeType = context.helper(TRANSITION)
} else if (isComponent) { } else if (isComponent) {
// user component w/ resolve // user component w/ resolve
context.helper(RESOLVE_COMPONENT) context.helper(RESOLVE_COMPONENT)

View File

@ -9,6 +9,7 @@ import { isKeepAlive } from './KeepAlive'
import { toRaw } from '@vue/reactivity' import { toRaw } from '@vue/reactivity'
import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling' import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
import { ShapeFlags } from '../shapeFlags' import { ShapeFlags } from '../shapeFlags'
import { onBeforeUnmount, onMounted } from '../apiLifecycle'
export interface BaseTransitionProps { export interface BaseTransitionProps {
mode?: 'in-out' | 'out-in' | 'default' mode?: 'in-out' | 'out-in' | 'default'
@ -22,9 +23,8 @@ export interface BaseTransitionProps {
persisted?: boolean persisted?: boolean
// Hooks. Using camel casef for easier usage in render functions & JSX. // Hooks. Using camel casef for easier usage in render functions & JSX.
// In templates these will be written as @before-enter="xxx" // In templates these can be written as @before-enter="xxx" as prop names
// The compiler has special handling to convert them into the proper cases. // are camelized
// enter
onBeforeEnter?: (el: any) => void onBeforeEnter?: (el: any) => void
onEnter?: (el: any, done: () => void) => void onEnter?: (el: any, done: () => void) => void
onAfterEnter?: (el: any) => void onAfterEnter?: (el: any) => void
@ -41,17 +41,29 @@ type TransitionHookCaller = (
args?: any[] args?: any[]
) => void ) => void
interface PendingCallbacks { interface TransitionState {
enter?: (cancelled?: boolean) => void isMounted: boolean
leave?: (cancelled?: boolean) => void isLeaving: boolean
isUnmounting: boolean
pendingEnter?: (cancelled?: boolean) => void
pendingLeave?: (cancelled?: boolean) => void
} }
const BaseTransitionImpl = { const BaseTransitionImpl = {
name: `BaseTransition`, name: `BaseTransition`,
setup(props: BaseTransitionProps, { slots }: SetupContext) { setup(props: BaseTransitionProps, { slots }: SetupContext) {
const instance = getCurrentInstance()! const instance = getCurrentInstance()!
const pendingCallbacks: PendingCallbacks = {} const state: TransitionState = {
let isLeaving = false isMounted: false,
isLeaving: false,
isUnmounting: false
}
onMounted(() => {
state.isMounted = true
})
onBeforeUnmount(() => {
state.isUnmounting = true
})
const callTransitionHook: TransitionHookCaller = (hook, args) => { const callTransitionHook: TransitionHookCaller = (hook, args) => {
hook && hook &&
@ -88,17 +100,17 @@ const BaseTransitionImpl = {
// at this point children has a guaranteed length of 1. // at this point children has a guaranteed length of 1.
const child = children[0] const child = children[0]
if (isLeaving) { if (state.isLeaving) {
return placeholder(child) return placeholder(child)
} }
let delayedLeave: (() => void) | undefined let delayedLeave: (() => void) | undefined
const performDelayedLeave = () => delayedLeave && delayedLeave() const performDelayedLeave = () => delayedLeave && delayedLeave()
const transitionHooks = (child.transition = resolveTransitionHooks( const transitionHooks = (child.transition = resolveTransitionHooks(
rawProps, rawProps,
state,
callTransitionHook, callTransitionHook,
instance.isMounted,
pendingCallbacks,
performDelayedLeave performDelayedLeave
)) ))
@ -118,10 +130,10 @@ const BaseTransitionImpl = {
updateHOCTransitionData(oldChild, transitionHooks) updateHOCTransitionData(oldChild, transitionHooks)
// switching between different views // switching between different views
if (mode === 'out-in') { if (mode === 'out-in') {
isLeaving = true state.isLeaving = true
// return placeholder node and queue update when leave finishes // return placeholder node and queue update when leave finishes
transitionHooks.afterLeave = () => { transitionHooks.afterLeave = () => {
isLeaving = false state.isLeaving = false
instance.update() instance.update()
} }
return placeholder(child) return placeholder(child)
@ -187,29 +199,28 @@ function resolveTransitionHooks(
onAfterLeave, onAfterLeave,
onLeaveCancelled onLeaveCancelled
}: BaseTransitionProps, }: BaseTransitionProps,
state: TransitionState,
callHook: TransitionHookCaller, callHook: TransitionHookCaller,
isMounted: boolean,
pendingCallbacks: PendingCallbacks,
performDelayedLeave: () => void performDelayedLeave: () => void
): TransitionHooks { ): TransitionHooks {
return { return {
persisted, persisted,
beforeEnter(el) { beforeEnter(el) {
if (!isMounted && !appear) { if (state.pendingLeave) {
return state.pendingLeave(true /* cancelled */)
} }
if (pendingCallbacks.leave) { if (!appear && !state.isMounted) {
pendingCallbacks.leave(true /* cancelled */) return
} }
callHook(onBeforeEnter, [el]) callHook(onBeforeEnter, [el])
}, },
enter(el) { enter(el) {
if (!isMounted && !appear) { if (!appear && !state.isMounted) {
return return
} }
let called = false let called = false
const afterEnter = (pendingCallbacks.enter = (cancelled?) => { const afterEnter = (state.pendingEnter = (cancelled?) => {
if (called) return if (called) return
called = true called = true
if (cancelled) { if (cancelled) {
@ -218,7 +229,7 @@ function resolveTransitionHooks(
callHook(onAfterEnter, [el]) callHook(onAfterEnter, [el])
performDelayedLeave() performDelayedLeave()
} }
pendingCallbacks.enter = undefined state.pendingEnter = undefined
}) })
if (onEnter) { if (onEnter) {
onEnter(el, afterEnter) onEnter(el, afterEnter)
@ -228,12 +239,15 @@ function resolveTransitionHooks(
}, },
leave(el, remove) { leave(el, remove) {
if (pendingCallbacks.enter) { if (state.pendingEnter) {
pendingCallbacks.enter(true /* cancelled */) state.pendingEnter(true /* cancelled */)
}
if (state.isUnmounting) {
return remove()
} }
callHook(onBeforeLeave, [el]) callHook(onBeforeLeave, [el])
let called = false let called = false
const afterLeave = (pendingCallbacks.leave = (cancelled?) => { const afterLeave = (state.pendingLeave = (cancelled?) => {
if (called) return if (called) return
called = true called = true
remove() remove()
@ -242,7 +256,7 @@ function resolveTransitionHooks(
} else { } else {
callHook(onAfterLeave, [el]) callHook(onAfterLeave, [el])
} }
pendingCallbacks.leave = undefined state.pendingLeave = undefined
}) })
if (onLeave) { if (onLeave) {
onLeave(el, afterLeave) onLeave(el, afterLeave)

View File

@ -16,6 +16,7 @@ const ANIMATION = 'animation'
export interface TransitionProps extends BaseTransitionProps { export interface TransitionProps extends BaseTransitionProps {
name?: string name?: string
type?: typeof TRANSITION | typeof ANIMATION type?: typeof TRANSITION | typeof ANIMATION
css?: boolean
duration?: number | { enter: number; leave: number } duration?: number | { enter: number; leave: number }
// custom transition classes // custom transition classes
enterFromClass?: string enterFromClass?: string
@ -41,6 +42,10 @@ if (__DEV__) {
...(BaseTransition as any).props, ...(BaseTransition as any).props,
name: String, name: String,
type: String, type: String,
// Cannot use Boolean otherwise it will be force casted to false when
// omitted
css: null,
duration: Object,
enterFromClass: String, enterFromClass: String,
enterActiveClass: String, enterActiveClass: String,
enterToClass: String, enterToClass: String,
@ -49,14 +54,14 @@ if (__DEV__) {
appearToClass: String, appearToClass: String,
leaveFromClass: String, leaveFromClass: String,
leaveActiveClass: String, leaveActiveClass: String,
leaveToClass: String, leaveToClass: String
duration: Object
} }
} }
function resolveTransitionProps({ function resolveTransitionProps({
name = 'v', name = 'v',
type, type,
css = true,
duration, duration,
enterFromClass = `${name}-enter-from`, enterFromClass = `${name}-enter-from`,
enterActiveClass = `${name}-enter-active`, enterActiveClass = `${name}-enter-active`,
@ -69,6 +74,10 @@ function resolveTransitionProps({
leaveToClass = `${name}-leave-to`, leaveToClass = `${name}-leave-to`,
...baseProps ...baseProps
}: TransitionProps): BaseTransitionProps { }: TransitionProps): BaseTransitionProps {
if (!css) {
return baseProps
}
const instance = getCurrentInstance()! const instance = getCurrentInstance()!
const durations = normalizeDuration(duration) const durations = normalizeDuration(duration)
const enterDuration = durations && durations[0] const enterDuration = durations && durations[0]