feat(transition): support component child

This commit is contained in:
Evan You 2019-11-20 21:56:17 -05:00
parent 93561b080e
commit 79f23a2f77
6 changed files with 114 additions and 111 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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
}
}

View File

@ -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<number | string, string> = {
[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',

View File

@ -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<HostNode = any, HostElement = any> {
@ -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,

View File

@ -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<HostNode, HostElement> =
@ -98,7 +93,7 @@ export interface VNode<HostNode = any, HostElement = any> {
component: ComponentInternalInstance | null
suspense: SuspenseBoundary<HostNode, HostElement> | null
dirs: DirectiveBinding[] | null
transition: TransitionProps | null
transition: TransitionData | null
// DOM
el: HostNode | null