From 1765985ec2c36ebc716f020d03013c75ed7b51ca Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 24 Nov 2019 18:37:59 -0500 Subject: [PATCH] feat(transition): support in templates --- packages/compiler-core/src/runtimeHelpers.ts | 4 ++ .../src/transforms/transformElement.ts | 17 +++-- .../src/components/BaseTransition.ts | 66 +++++++++++-------- .../runtime-dom/src/components/Transition.ts | 13 +++- 4 files changed, 67 insertions(+), 33 deletions(-) diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts index a6d679ef9..9ee8a4526 100644 --- a/packages/compiler-core/src/runtimeHelpers.ts +++ b/packages/compiler-core/src/runtimeHelpers.ts @@ -2,6 +2,8 @@ export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``) 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` : ``) @@ -30,6 +32,8 @@ export const helperNameMap: any = { [PORTAL]: `Portal`, [SUSPENSE]: `Suspense`, [KEEP_ALIVE]: `KeepAlive`, + [TRANSITION]: `Transition`, + [BASE_TRANSITION]: `BaseTransition`, [OPEN_BLOCK]: `openBlock`, [CREATE_BLOCK]: `createBlock`, [CREATE_VNODE]: `createVNode`, diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index e7935a636..e70588bbe 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -15,7 +15,7 @@ import { 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, @@ -27,7 +27,8 @@ import { TO_HANDLERS, PORTAL, SUSPENSE, - KEEP_ALIVE + KEEP_ALIVE, + TRANSITION } from '../runtimeHelpers' import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils' import { buildSlots } from './vSlot' @@ -37,6 +38,9 @@ import { isStaticNode } from './hoistStatic' // import, which should be used instead of a resolveDirective call. const directiveImportMap = new WeakMap() +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 ( @@ -53,9 +57,10 @@ export const transformElement: NodeTransform = (node, context) => { // 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 @@ -96,6 +101,8 @@ export const transformElement: NodeTransform = (node, context) => { 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) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 9ffa165c6..528e9e906 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -9,6 +9,7 @@ import { isKeepAlive } from './KeepAlive' 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' @@ -22,9 +23,8 @@ export interface BaseTransitionProps { 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 @@ -41,17 +41,29 @@ type TransitionHookCaller = ( 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 && @@ -88,17 +100,17 @@ const BaseTransitionImpl = { // 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 )) @@ -118,10 +130,10 @@ const BaseTransitionImpl = { 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) @@ -187,29 +199,28 @@ function resolveTransitionHooks( 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) { @@ -218,7 +229,7 @@ function resolveTransitionHooks( callHook(onAfterEnter, [el]) performDelayedLeave() } - pendingCallbacks.enter = undefined + state.pendingEnter = undefined }) if (onEnter) { onEnter(el, afterEnter) @@ -228,12 +239,15 @@ function resolveTransitionHooks( }, 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() @@ -242,7 +256,7 @@ function resolveTransitionHooks( } else { callHook(onAfterLeave, [el]) } - pendingCallbacks.leave = undefined + state.pendingLeave = undefined }) if (onLeave) { onLeave(el, afterLeave) diff --git a/packages/runtime-dom/src/components/Transition.ts b/packages/runtime-dom/src/components/Transition.ts index b61fe25b4..18f15ddce 100644 --- a/packages/runtime-dom/src/components/Transition.ts +++ b/packages/runtime-dom/src/components/Transition.ts @@ -16,6 +16,7 @@ const ANIMATION = 'animation' 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 @@ -41,6 +42,10 @@ if (__DEV__) { ...(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, @@ -49,14 +54,14 @@ if (__DEV__) { 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`, @@ -69,6 +74,10 @@ function resolveTransitionProps({ leaveToClass = `${name}-leave-to`, ...baseProps }: TransitionProps): BaseTransitionProps { + if (!css) { + return baseProps + } + const instance = getCurrentInstance()! const durations = normalizeDuration(duration) const enterDuration = durations && durations[0]