From 79f23a2f7758912714492c8240d81df4e244c3cf Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 20 Nov 2019 21:56:17 -0500 Subject: [PATCH] feat(transition): support component child --- packages/runtime-core/src/componentProps.ts | 5 +- .../runtime-core/src/componentRenderUtils.ts | 6 + .../runtime-core/src/components/Transition.ts | 128 +++++++++++------- packages/runtime-core/src/errorHandling.ts | 2 + packages/runtime-core/src/renderer.ts | 75 +++------- packages/runtime-core/src/vnode.ts | 9 +- 6 files changed, 114 insertions(+), 111 deletions(-) diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index ead0784db6..5d921f3bfa 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -8,7 +8,6 @@ import { isFunction, isArray, isObject, - isReservedProp, hasOwn, toRawType, PatchFlags, @@ -122,8 +121,8 @@ export function resolveProps( if (rawProps != null) { for (const key in rawProps) { - // key, ref are reserved - if (isReservedProp(key)) continue + // key, ref are reserved and never passed down + if (key === 'key' || key === 'ref') continue // prop option names are camelized during normalization, so to support // kebab -> camel conversion here we need to camelize the key. const camelKey = camelize(key) diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index e5964d0db1..d42982d580 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -85,6 +85,12 @@ export function renderComponentRoot( ) } } + + // inherit transition data + if (vnode.transition != null) { + // TODO warn if component has transition data but root is a fragment + result.transition = vnode.transition + } } catch (err) { handleError(err, instance, ErrorCodes.RENDER_FUNCTION) result = createVNode(Comment) diff --git a/packages/runtime-core/src/components/Transition.ts b/packages/runtime-core/src/components/Transition.ts index 8b96af7ca6..c02265fda8 100644 --- a/packages/runtime-core/src/components/Transition.ts +++ b/packages/runtime-core/src/components/Transition.ts @@ -1,17 +1,12 @@ import { createComponent } from '../apiCreateComponent' -import { getCurrentInstance } from '../component' -import { - cloneVNode, - Comment, - isSameVNodeType, - VNodeProps, - VNode, - mergeProps -} from '../vnode' +import { getCurrentInstance, ComponentInternalInstance } from '../component' +import { cloneVNode, Comment, isSameVNodeType, VNode } from '../vnode' import { warn } from '../warning' import { isKeepAlive } from './KeepAlive' import { toRaw } from '@vue/reactivity' import { onMounted } from '../apiLifecycle' +import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling' +import { ShapeFlags } from '../shapeFlags' // Using camel case here makes it easier to use in render functions & JSX. // In templates these will be written as @before-enter="xxx" @@ -66,52 +61,51 @@ export const Transition = createComponent({ } // at this point children has a guaranteed length of 1. - const rawChild = children[0] + const child = children[0] if (isLeaving) { - return placeholder(rawChild) + return placeholder(child) } - rawChild.transition = rawProps + let delayedLeave: (() => void) | undefined + const performDelayedLeave = () => delayedLeave && delayedLeave() + const transitionData = (child.transition = resolveTransitionData( + instance, + rawProps, + isMounted, + performDelayedLeave + )) + // clone old subTree because we need to modify it const oldChild = instance.subTree ? (instance.subTree = cloneVNode(instance.subTree)) : null // handle mode - let performDelayedLeave: (() => void) | undefined if ( oldChild && - !isSameVNodeType(rawChild, oldChild) && + !isSameVNodeType(child, oldChild) && oldChild.type !== Comment ) { // update old tree's hooks in case of dynamic transition - oldChild.transition = rawProps + // need to do this recursively in case of HOCs + updateHOCTransitionData(oldChild, transitionData) // switching between different views if (mode === 'out-in') { isLeaving = true // return placeholder node and queue update when leave finishes - oldChild.props = mergeProps(oldChild.props!, { - onVnodeRemoved() { - isLeaving = false - instance.update() - } - }) - return placeholder(rawChild) + transitionData.afterLeave = () => { + isLeaving = false + instance.update() + } + return placeholder(child) } else if (mode === 'in-out') { - let delayedLeave: () => void - performDelayedLeave = () => delayedLeave() - oldChild.props = mergeProps(oldChild.props!, { - onVnodeDelayLeave(performLeave) { - delayedLeave = performLeave - } - }) + transitionData.delayLeave = performLeave => { + delayedLeave = performLeave + } } } - return cloneVNode( - rawChild, - resolveTransitionInjections(rawProps, isMounted, performDelayedLeave) - ) + return child } } }) @@ -133,7 +127,16 @@ if (__DEV__) { } } -function resolveTransitionInjections( +export interface TransitionData { + beforeEnter(el: object): void + enter(el: object): void + leave(el: object, remove: () => void): void + afterLeave?(): void + delayLeave?(performLeave: () => void): void +} + +function resolveTransitionData( + instance: ComponentInternalInstance, { appear, onBeforeEnter, @@ -146,24 +149,35 @@ function resolveTransitionInjections( onLeaveCancelled }: TransitionProps, isMounted: boolean, - performDelayedLeave?: () => void -): VNodeProps { - // TODO handle appear + performDelayedLeave: () => void +): TransitionData { // TODO handle cancel hooks return { - onVnodeBeforeMount(vnode) { + beforeEnter(el) { if (!isMounted && !appear) { return } - onBeforeEnter && onBeforeEnter(vnode.el) + onBeforeEnter && + callWithAsyncErrorHandling( + onBeforeEnter, + instance, + ErrorCodes.TRANSITION_HOOK, + [el] + ) }, - onVnodeMounted({ el }) { + enter(el) { if (!isMounted && !appear) { return } const done = () => { - onAfterEnter && onAfterEnter(el) - performDelayedLeave && performDelayedLeave() + onAfterEnter && + callWithAsyncErrorHandling( + onAfterEnter, + instance, + ErrorCodes.TRANSITION_HOOK, + [el] + ) + performDelayedLeave() } if (onEnter) { onEnter(el, done) @@ -171,16 +185,30 @@ function resolveTransitionInjections( done() } }, - onVnodeBeforeRemove({ el }, remove) { - onBeforeLeave && onBeforeLeave(el) + leave(el, remove) { + onBeforeLeave && + callWithAsyncErrorHandling( + onBeforeLeave, + instance, + ErrorCodes.TRANSITION_HOOK, + [el] + ) + const afterLeave = () => + onAfterLeave && + callWithAsyncErrorHandling( + onAfterLeave, + instance, + ErrorCodes.TRANSITION_HOOK, + [el] + ) if (onLeave) { onLeave(el, () => { remove() - onAfterLeave && onAfterLeave(el) + afterLeave() }) } else { remove() - onAfterLeave && onAfterLeave(el) + afterLeave() } } } @@ -197,3 +225,11 @@ function placeholder(vnode: VNode): VNode | undefined { return vnode } } + +function updateHOCTransitionData(vnode: VNode, data: TransitionData) { + if (vnode.shapeFlag & ShapeFlags.COMPONENT) { + updateHOCTransitionData(vnode.component!.subTree, data) + } else { + vnode.transition = data + } +} diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index 88d5af350e..c71524ac88 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -14,6 +14,7 @@ export const enum ErrorCodes { NATIVE_EVENT_HANDLER, COMPONENT_EVENT_HANDLER, DIRECTIVE_HOOK, + TRANSITION_HOOK, APP_ERROR_HANDLER, APP_WARN_HANDLER, FUNCTION_REF, @@ -42,6 +43,7 @@ export const ErrorTypeStrings: Record = { [ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler', [ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler', [ErrorCodes.DIRECTIVE_HOOK]: 'directive hook', + [ErrorCodes.TRANSITION_HOOK]: 'transition hook', [ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler', [ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler', [ErrorCodes.FUNCTION_REF]: 'ref function', diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 5ca2ca0631..da3e777cec 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -27,8 +27,7 @@ import { EMPTY_ARR, isReservedProp, isFunction, - PatchFlags, - isArray + PatchFlags } from '@vue/shared' import { queueJob, queuePostFlushCb, flushPostFlushCbs } from './scheduler' import { @@ -52,11 +51,7 @@ import { queueEffectWithSuspense, SuspenseImpl } from './components/Suspense' -import { - ErrorCodes, - callWithErrorHandling, - callWithAsyncErrorHandling -} from './errorHandling' +import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { KeepAliveSink, isKeepAlive } from './components/KeepAlive' export interface RendererOptions { @@ -362,7 +357,7 @@ export function createRenderer< const tag = vnode.type as string isSVG = isSVG || tag === 'svg' const el = (vnode.el = hostCreateElement(tag, isSVG)) - const { props, shapeFlag } = vnode + const { props, shapeFlag, transition } = vnode if (props != null) { for (const key in props) { if (isReservedProp(key)) continue @@ -372,6 +367,9 @@ export function createRenderer< invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode) } } + if (transition != null) { + transition.beforeEnter(el) + } if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(el, vnode.children as string) } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { @@ -386,9 +384,12 @@ export function createRenderer< ) } hostInsert(el, container, anchor) - if (props != null && props.onVnodeMounted != null) { + const vnodeMountedHook = props && props.onVnodeMounted + if (vnodeMountedHook != null || transition != null) { queuePostRenderEffect(() => { - invokeDirectiveHook(props.onVnodeMounted!, parentComponent, vnode) + vnodeMountedHook && + invokeDirectiveHook(vnodeMountedHook, parentComponent, vnode) + transition && transition.enter(el) }, parentSuspense) } } @@ -1412,13 +1413,15 @@ export function createRenderer< doRemove?: boolean ) { const { + el, props, ref, type, children, dynamicChildren, shapeFlag, - anchor + anchor, + transition } = vnode // unset ref @@ -1462,23 +1465,16 @@ export function createRenderer< } if (doRemove) { - const beforeRemoveHooks = props && props.onVnodeBeforeRemove const remove = () => { hostRemove(vnode.el!) if (anchor != null) hostRemove(anchor) - const removedHook = props && props.onVnodeRemoved - removedHook && removedHook() - } - if (vnode.shapeFlag & ShapeFlags.ELEMENT && beforeRemoveHooks != null) { - const delayLeave = props && props.onVnodeDelayLeave - const performLeave = () => { - invokeBeforeRemoveHooks( - beforeRemoveHooks, - parentComponent, - vnode, - remove - ) + if (transition != null && transition.afterLeave) { + transition.afterLeave() } + } + if (vnode.shapeFlag & ShapeFlags.ELEMENT && transition != null) { + const { leave, delayLeave } = transition + const performLeave = () => leave(el!, remove) if (delayLeave) { delayLeave(performLeave) } else { @@ -1496,37 +1492,6 @@ export function createRenderer< } } - function invokeBeforeRemoveHooks( - hooks: ((...args: any[]) => any) | ((...args: any[]) => any)[], - instance: ComponentInternalInstance | null, - vnode: HostVNode, - done: () => void - ) { - if (!isArray(hooks)) { - hooks = [hooks] - } - let delayedRemoveCount = hooks.length - const doneRemove = () => { - delayedRemoveCount-- - if (allHooksCalled && !delayedRemoveCount) { - done() - } - } - let allHooksCalled = false - for (let i = 0; i < hooks.length; i++) { - callWithAsyncErrorHandling( - hooks[i], - instance, - ErrorCodes.DIRECTIVE_HOOK, - [vnode, doneRemove] - ) - } - allHooksCalled = true - if (!delayedRemoveCount) { - done() - } - } - function unmountComponent( instance: ComponentInternalInstance, parentSuspense: HostSuspenseBoundary | null, diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 3134057fee..c7b32bdfe4 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -19,7 +19,7 @@ import { AppContext } from './apiApp' import { SuspenseBoundary } from './components/Suspense' import { DirectiveBinding } from './directives' import { SuspenseImpl } from './components/Suspense' -import { TransitionProps } from './components/Transition' +import { TransitionData } from './components/Transition' export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { __isFragment: true @@ -57,11 +57,6 @@ export interface VNodeProps { onVnodeUpdated?: (vnode: VNode, oldVNode: VNode) => void onVnodeBeforeUnmount?: (vnode: VNode) => void onVnodeUnmounted?: (vnode: VNode) => void - - // transition hooks, internal. - onVnodeDelayLeave?: (performLeave: () => void) => void - onVnodeBeforeRemove?: (vnode: VNode, remove: () => void) => void - onVnodeRemoved?: () => void } type VNodeChildAtom = @@ -98,7 +93,7 @@ export interface VNode { component: ComponentInternalInstance | null suspense: SuspenseBoundary | null dirs: DirectiveBinding[] | null - transition: TransitionProps | null + transition: TransitionData | null // DOM el: HostNode | null -- 2.47.3