feat(transition): base transition component
This commit is contained in:
@@ -39,6 +39,9 @@ export interface KeepAliveSink {
|
||||
deactivate: (vnode: VNode) => void
|
||||
}
|
||||
|
||||
export const isKeepAlive = (vnode: VNode): boolean =>
|
||||
(vnode.type as any).__isKeepAlive
|
||||
|
||||
const KeepAliveImpl = {
|
||||
name: `KeepAlive`,
|
||||
|
||||
@@ -47,6 +50,12 @@ const KeepAliveImpl = {
|
||||
// would prevent it from being tree-shaken.
|
||||
__isKeepAlive: true,
|
||||
|
||||
props: {
|
||||
include: [String, RegExp, Array],
|
||||
exclude: [String, RegExp, Array],
|
||||
max: [String, Number]
|
||||
},
|
||||
|
||||
setup(props: KeepAliveProps, { slots }: SetupContext) {
|
||||
const cache: Cache = new Map()
|
||||
const keys: Keys = new Set()
|
||||
@@ -200,14 +209,6 @@ const KeepAliveImpl = {
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
;(KeepAliveImpl as any).props = {
|
||||
include: [String, RegExp, Array],
|
||||
exclude: [String, RegExp, Array],
|
||||
max: [String, Number]
|
||||
}
|
||||
}
|
||||
|
||||
// export the public type for h/tsx inference
|
||||
export const KeepAlive = (KeepAliveImpl as any) as {
|
||||
new (): {
|
||||
|
||||
199
packages/runtime-core/src/components/Transition.ts
Normal file
199
packages/runtime-core/src/components/Transition.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { createComponent } from '../apiCreateComponent'
|
||||
import { getCurrentInstance } from '../component'
|
||||
import {
|
||||
cloneVNode,
|
||||
Comment,
|
||||
isSameVNodeType,
|
||||
VNodeProps,
|
||||
VNode,
|
||||
mergeProps
|
||||
} from '../vnode'
|
||||
import { warn } from '../warning'
|
||||
import { isKeepAlive } from './KeepAlive'
|
||||
import { toRaw } from '@vue/reactivity'
|
||||
import { onMounted } from '../apiLifecycle'
|
||||
|
||||
// Using camel case here makes it easier to use 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.
|
||||
export interface TransitionProps {
|
||||
mode?: 'in-out' | 'out-in' | 'default'
|
||||
appear?: boolean
|
||||
// enter
|
||||
onBeforeEnter?: (el: any) => void
|
||||
onEnter?: (el: any, done: () => void) => void
|
||||
onAfterEnter?: (el: any) => void
|
||||
onEnterCancelled?: (el: any) => void
|
||||
// leave
|
||||
onBeforeLeave?: (el: any) => void
|
||||
onLeave?: (el: any, done: () => void) => void
|
||||
onAfterLeave?: (el: any) => void
|
||||
onLeaveCancelled?: (el: any) => void
|
||||
}
|
||||
|
||||
export const Transition = createComponent({
|
||||
name: `Transition`,
|
||||
setup(props: TransitionProps, { slots }) {
|
||||
const instance = getCurrentInstance()!
|
||||
let isLeaving = false
|
||||
let isMounted = false
|
||||
|
||||
onMounted(() => {
|
||||
isMounted = true
|
||||
})
|
||||
|
||||
return () => {
|
||||
const children = slots.default && slots.default()
|
||||
if (!children || !children.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// warn multiple elements
|
||||
if (__DEV__ && children.length > 1) {
|
||||
warn(
|
||||
'<transition> can only be used on a single element. Use ' +
|
||||
'<transition-group> for lists.'
|
||||
)
|
||||
}
|
||||
|
||||
// there's no need to track reactivity for these props so use the raw
|
||||
// props for a bit better perf
|
||||
const rawProps = toRaw(props)
|
||||
const { mode } = rawProps
|
||||
// check mode
|
||||
if (__DEV__ && mode && !['in-out', 'out-in', 'default'].includes(mode)) {
|
||||
warn(`invalid <transition> mode: ${mode}`)
|
||||
}
|
||||
|
||||
// at this point children has a guaranteed length of 1.
|
||||
const rawChild = children[0]
|
||||
if (isLeaving) {
|
||||
return placeholder(rawChild)
|
||||
}
|
||||
|
||||
rawChild.transition = rawProps
|
||||
// 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) &&
|
||||
oldChild.type !== Comment
|
||||
) {
|
||||
// update old tree's hooks in case of dynamic transition
|
||||
oldChild.transition = rawProps
|
||||
// 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)
|
||||
} else if (mode === 'in-out') {
|
||||
let delayedLeave: () => void
|
||||
performDelayedLeave = () => delayedLeave()
|
||||
oldChild.props = mergeProps(oldChild.props!, {
|
||||
onVnodeDelayLeave(performLeave) {
|
||||
delayedLeave = performLeave
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return cloneVNode(
|
||||
rawChild,
|
||||
resolveTransitionInjections(rawProps, isMounted, performDelayedLeave)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (__DEV__) {
|
||||
;(Transition as any).props = {
|
||||
mode: String,
|
||||
appear: Boolean,
|
||||
// enter
|
||||
onBeforeEnter: Function,
|
||||
onEnter: Function,
|
||||
onAfterEnter: Function,
|
||||
onEnterCancelled: Function,
|
||||
// leave
|
||||
onBeforeLeave: Function,
|
||||
onLeave: Function,
|
||||
onAfterLeave: Function,
|
||||
onLeaveCancelled: Function
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTransitionInjections(
|
||||
{
|
||||
appear,
|
||||
onBeforeEnter,
|
||||
onEnter,
|
||||
onAfterEnter,
|
||||
onEnterCancelled,
|
||||
onBeforeLeave,
|
||||
onLeave,
|
||||
onAfterLeave,
|
||||
onLeaveCancelled
|
||||
}: TransitionProps,
|
||||
isMounted: boolean,
|
||||
performDelayedLeave?: () => void
|
||||
): VNodeProps {
|
||||
// TODO handle appear
|
||||
// TODO handle cancel hooks
|
||||
return {
|
||||
onVnodeBeforeMount(vnode) {
|
||||
if (!isMounted && !appear) {
|
||||
return
|
||||
}
|
||||
onBeforeEnter && onBeforeEnter(vnode.el)
|
||||
},
|
||||
onVnodeMounted({ el }) {
|
||||
if (!isMounted && !appear) {
|
||||
return
|
||||
}
|
||||
const done = () => {
|
||||
onAfterEnter && onAfterEnter(el)
|
||||
performDelayedLeave && performDelayedLeave()
|
||||
}
|
||||
if (onEnter) {
|
||||
onEnter(el, done)
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
},
|
||||
onVnodeBeforeRemove({ el }, remove) {
|
||||
onBeforeLeave && onBeforeLeave(el)
|
||||
if (onLeave) {
|
||||
onLeave(el, () => {
|
||||
remove()
|
||||
onAfterLeave && onAfterLeave(el)
|
||||
})
|
||||
} else {
|
||||
remove()
|
||||
onAfterLeave && onAfterLeave(el)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the placeholder really only handles one special case: KeepAlive
|
||||
// in the case of a KeepAlive in a leave phase we need to return a KeepAlive
|
||||
// placeholder with empty content to avoid the KeepAlive instance from being
|
||||
// unmounted.
|
||||
function placeholder(vnode: VNode): VNode | undefined {
|
||||
if (isKeepAlive(vnode)) {
|
||||
vnode = cloneVNode(vnode)
|
||||
vnode.children = null
|
||||
return vnode
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user