From 41822e3743eb68d927a14ae72a39bbf553d0bbb8 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 27 Feb 2025 16:41:33 +0800 Subject: [PATCH] feat(vapor): vapor transition --- .../src/generators/component.ts | 3 +- .../src/components/BaseTransition.ts | 3 +- packages/runtime-core/src/index.ts | 4 + packages/runtime-core/src/renderer.ts | 99 ++++++++++++------- .../runtime-dom/src/components/Transition.ts | 24 ++++- packages/runtime-dom/src/index.ts | 12 +++ packages/runtime-vapor/src/apiCreateApp.ts | 2 + packages/runtime-vapor/src/block.ts | 52 +++++++--- packages/runtime-vapor/src/component.ts | 9 +- .../src/components/Transition.ts | 53 ++++++++++ .../runtime-vapor/src/directives/vShow.ts | 15 ++- packages/runtime-vapor/src/vdomInterop.ts | 2 + 12 files changed, 224 insertions(+), 54 deletions(-) create mode 100644 packages/runtime-vapor/src/components/Transition.ts diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 73e23150fa..b131ad2e94 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -52,13 +52,12 @@ export function genCreateComponent( const [ids, handlers] = processInlineHandlers(props, context) const rawProps = context.withId(() => genRawProps(props, context), ids) const inlineHandlers: CodeFragment[] = handlers.reduce( - (acc, { name, value }) => { + (acc, { name, value }: InlineHandler) => { const handler = genEventHandler(context, value, undefined, false) return [...acc, `const ${name} = `, ...handler, NEWLINE] }, [], ) - return [ NEWLINE, ...inlineHandlers, diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 2b58bc3fc4..ae89f36356 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -1,6 +1,7 @@ import { type ComponentInternalInstance, type ComponentOptions, + type GenericComponentInstance, type SetupContext, getCurrentInstance, } from '../component' @@ -324,7 +325,7 @@ export function resolveTransitionHooks( vnode: VNode, props: BaseTransitionProps, state: TransitionState, - instance: ComponentInternalInstance, + instance: GenericComponentInstance, postClone?: (hooks: TransitionHooks) => void, ): TransitionHooks { const { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index c7150e38e8..51f42562ee 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -557,3 +557,7 @@ export { startMeasure, endMeasure } from './profiling' * @internal */ export { initFeatureFlags } from './featureFlags' +/** + * @internal + */ +export { applyTransitionEnter, applyTransitionLeave } from './renderer' diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index fcbfdd0426..fc3664de8c 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -731,19 +731,20 @@ function baseCreateRenderer( } // #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved // #1689 For inside suspense + suspense resolved case, just call it - const needCallTransitionHooks = needTransition(parentSuspense, transition) - if (needCallTransitionHooks) { - transition!.beforeEnter(el) + if (transition) { + applyTransitionEnter( + el, + transition, + () => hostInsert(el, container, anchor), + parentSuspense, + ) + } else { + hostInsert(el, container, anchor) } - hostInsert(el, container, anchor) - if ( - (vnodeHook = props && props.onVnodeMounted) || - needCallTransitionHooks || - dirs - ) { + + if ((vnodeHook = props && props.onVnodeMounted) || dirs) { queuePostRenderEffect(() => { vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) - needCallTransitionHooks && transition!.enter(el) dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') }, parentSuspense) } @@ -2115,9 +2116,12 @@ function baseCreateRenderer( transition if (needTransition) { if (moveType === MoveType.ENTER) { - transition!.beforeEnter(el!) - hostInsert(el!, container, anchor) - queuePostRenderEffect(() => transition!.enter(el!), parentSuspense) + applyTransitionEnter( + el!, + transition, + () => hostInsert(el!, container, anchor), + parentSuspense, + ) } else { const { leave, delayLeave, afterLeave } = transition! const remove = () => hostInsert(el!, container, anchor) @@ -2292,27 +2296,15 @@ function baseCreateRenderer( return } - const performRemove = () => { - hostRemove(el!) - if (transition && !transition.persisted && transition.afterLeave) { - transition.afterLeave() - } - } - - if ( - vnode.shapeFlag & ShapeFlags.ELEMENT && - transition && - !transition.persisted - ) { - const { leave, delayLeave } = transition - const performLeave = () => leave(el!, performRemove) - if (delayLeave) { - delayLeave(vnode.el!, performRemove, performLeave) - } else { - performLeave() - } + if (transition) { + applyTransitionLeave( + el!, + transition, + () => hostRemove(el!), + !!(vnode.shapeFlag & ShapeFlags.ELEMENT), + ) } else { - performRemove() + hostRemove(el!) } } @@ -2630,6 +2622,47 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void { } } +export function applyTransitionEnter( + el: RendererElement, + transition: TransitionHooks, + insert: () => void, + parentSuspense: SuspenseBoundary | null, +): void { + if (needTransition(parentSuspense, transition)) { + transition.beforeEnter(el) + insert() + queuePostRenderEffect(() => transition.enter(el), parentSuspense) + } else { + insert() + } +} + +export function applyTransitionLeave( + el: RendererElement, + transition: TransitionHooks, + remove: () => void, + isElement: boolean = true, +): void { + const performRemove = () => { + remove() + if (transition && !transition.persisted && transition.afterLeave) { + transition.afterLeave() + } + } + + if (isElement && transition && !transition.persisted) { + const { leave, delayLeave } = transition + const performLeave = () => leave(el, performRemove) + if (delayLeave) { + delayLeave(el, performRemove, performLeave) + } else { + performLeave() + } + } else { + performRemove() + } +} + function getVaporInterface( instance: ComponentInternalInstance | null, vnode: VNode, diff --git a/packages/runtime-dom/src/components/Transition.ts b/packages/runtime-dom/src/components/Transition.ts index 6c6344bfca..90cdaba4e7 100644 --- a/packages/runtime-dom/src/components/Transition.ts +++ b/packages/runtime-dom/src/components/Transition.ts @@ -32,6 +32,20 @@ export interface TransitionProps extends BaseTransitionProps { leaveToClass?: string } +export interface VaporTransitionInterface { + applyTransition: ( + props: TransitionProps, + slots: { default: () => any }, + ) => void +} + +let vaporTransitionImpl: VaporTransitionInterface | null = null +export const registerVaporTransition = ( + impl: VaporTransitionInterface, +): void => { + vaporTransitionImpl = impl +} + export const vtcKey: unique symbol = Symbol('_vtc') export interface ElementWithTransition extends HTMLElement { @@ -85,9 +99,13 @@ const decorate = (t: typeof Transition) => { * base Transition component, with DOM-specific logic. */ export const Transition: FunctionalComponent = - /*@__PURE__*/ decorate((props, { slots }) => - h(BaseTransition, resolveTransitionProps(props), slots), - ) + /*@__PURE__*/ decorate((props, { slots, vapor }: any) => { + const resolvedProps = resolveTransitionProps(props) + if (vapor) { + return vaporTransitionImpl!.applyTransition(resolvedProps, slots) + } + return h(BaseTransition, resolvedProps, slots) + }) /** * #3227 Incoming hooks may be merged into arrays when wrapping Transition diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 51c72fe2ed..0cfd08e87a 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -348,3 +348,15 @@ export { vModelSelectInit, vModelSetSelected, } from './directives/vModel' +/** + * @internal + */ +export { + resolveTransitionProps, + TransitionPropsValidators, + registerVaporTransition, +} from './components/Transition' +/** + * @internal + */ +export type { VaporTransitionInterface } from './components/Transition' diff --git a/packages/runtime-vapor/src/apiCreateApp.ts b/packages/runtime-vapor/src/apiCreateApp.ts index 8088e1aee6..da09a79a12 100644 --- a/packages/runtime-vapor/src/apiCreateApp.ts +++ b/packages/runtime-vapor/src/apiCreateApp.ts @@ -20,11 +20,13 @@ import { import type { RawProps } from './componentProps' import { getGlobalThis } from '@vue/shared' import { optimizePropertyLookup } from './dom/prop' +import { ensureVaporTransition } from './components/Transition' let _createApp: CreateAppFunction const mountApp: AppMountFn = (app, container) => { optimizePropertyLookup() + ensureVaporTransition() // clear content before mounting if (container.nodeType === 1 /* Node.ELEMENT_NODE */) { diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index af18870559..a4cfcea1f9 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -7,13 +7,19 @@ import { } from './component' import { createComment, createTextNode } from './dom/node' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' +import { + type TransitionHooks, + applyTransitionEnter, + applyTransitionLeave, +} from '@vue/runtime-dom' -export type Block = +export type Block = ( | Node | VaporFragment | DynamicFragment | VaporComponentInstance | Block[] +) & { transition?: TransitionHooks } export type BlockFn = (...args: any[]) => Block @@ -22,6 +28,7 @@ export class VaporFragment { anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void + transition?: TransitionHooks constructor(nodes: Block) { this.nodes = nodes @@ -52,13 +59,13 @@ export class DynamicFragment extends VaporFragment { // teardown previous branch if (this.scope) { this.scope.stop() - parent && remove(this.nodes, parent) + parent && remove(this.nodes, parent, this.transition) } if (render) { this.scope = new EffectScope() this.nodes = this.scope.run(render) || [] - if (parent) insert(this.nodes, parent, this.anchor) + if (parent) insert(this.nodes, parent, this.anchor, this.transition) } else { this.scope = undefined this.nodes = [] @@ -69,7 +76,7 @@ export class DynamicFragment extends VaporFragment { this.nodes = (this.scope || (this.scope = new EffectScope())).run(this.fallback) || [] - parent && insert(this.nodes, parent, this.anchor) + parent && insert(this.nodes, parent, this.anchor, this.transition) } resetTracking() @@ -106,12 +113,23 @@ export function insert( block: Block, parent: ParentNode, anchor: Node | null | 0 = null, // 0 means prepend + transition: TransitionHooks | undefined = block.transition, + parentSuspense?: any, // TODO Suspense ): void { anchor = anchor === 0 ? parent.firstChild : anchor if (block instanceof Node) { - parent.insertBefore(block, anchor) + if (transition) { + applyTransitionEnter( + block, + transition, + () => parent.insertBefore(block, anchor), + parentSuspense, + ) + } else { + parent.insertBefore(block, anchor) + } } else if (isVaporComponent(block)) { - mountComponent(block, parent, anchor) + mountComponent(block, parent, anchor, transition) } else if (isArray(block)) { for (let i = 0; i < block.length; i++) { insert(block[i], parent, anchor) @@ -121,7 +139,7 @@ export function insert( if (block.insert) { block.insert(parent, anchor) } else { - insert(block.nodes, parent, anchor) + insert(block.nodes, parent, anchor, block.transition) } if (block.anchor) insert(block.anchor, parent, anchor) } @@ -132,11 +150,23 @@ export function prepend(parent: ParentNode, ...blocks: Block[]): void { while (i--) insert(blocks[i], parent, 0) } -export function remove(block: Block, parent?: ParentNode): void { +export function remove( + block: Block, + parent?: ParentNode, + transition: TransitionHooks | undefined = block.transition, +): void { if (block instanceof Node) { - parent && parent.removeChild(block) + if (transition) { + applyTransitionLeave( + block, + transition, + () => parent && parent.removeChild(block), + ) + } else { + parent && parent.removeChild(block) + } } else if (isVaporComponent(block)) { - unmountComponent(block, parent) + unmountComponent(block, parent, transition) } else if (isArray(block)) { for (let i = 0; i < block.length; i++) { remove(block[i], parent) @@ -146,7 +176,7 @@ export function remove(block: Block, parent?: ParentNode): void { if (block.remove) { block.remove(parent) } else { - remove(block.nodes, parent) + remove(block.nodes, parent, block.transition) } if (block.anchor) remove(block.anchor, parent) if ((block as DynamicFragment).scope) { diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 3c39612bb8..2c448bda24 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -11,6 +11,7 @@ import { type NormalizedPropsOptions, type ObjectEmitsOptions, type SuspenseBoundary, + type TransitionHooks, callWithErrorHandling, currentInstance, endMeasure, @@ -475,17 +476,18 @@ export function mountComponent( instance: VaporComponentInstance, parent: ParentNode, anchor?: Node | null | 0, + transition?: TransitionHooks, ): void { if (__DEV__) { startMeasure(instance, `mount`) } if (!instance.isMounted) { if (instance.bm) invokeArrayFns(instance.bm) - insert(instance.block, parent, anchor) + insert(instance.block, parent, anchor, transition) if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!)) instance.isMounted = true } else { - insert(instance.block, parent, anchor) + insert(instance.block, parent, anchor, transition) } if (__DEV__) { endMeasure(instance, `mount`) @@ -495,6 +497,7 @@ export function mountComponent( export function unmountComponent( instance: VaporComponentInstance, parentNode?: ParentNode, + transition?: TransitionHooks, ): void { if (instance.isMounted && !instance.isUnmounted) { if (__DEV__ && instance.type.__hmrId) { @@ -513,7 +516,7 @@ export function unmountComponent( } if (parentNode) { - remove(instance.block, parentNode) + remove(instance.block, parentNode, transition) } } diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts new file mode 100644 index 0000000000..2eb87ebffe --- /dev/null +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -0,0 +1,53 @@ +import { + type TransitionHooks, + type TransitionProps, + type VaporTransitionInterface, + currentInstance, + registerVaporTransition, + resolveTransitionHooks, + useTransitionState, +} from '@vue/runtime-dom' +import type { Block } from '../block' +import { isVaporComponent } from '../component' + +export const vaporTransitionImpl: VaporTransitionInterface = { + applyTransition: (props: TransitionProps, slots: { default: () => any }) => { + const children = slots.default && slots.default() + if (!children) { + return + } + + // TODO find non-comment node + const child = children + + const state = useTransitionState() + let enterHooks = resolveTransitionHooks( + child as any, + props, + state, + currentInstance!, + hooks => (enterHooks = hooks), + ) + setTransitionHooks(child, enterHooks) + + // TODO handle mode + + return children + }, +} + +function setTransitionHooks(block: Block, hooks: TransitionHooks) { + if (isVaporComponent(block)) { + setTransitionHooks(block.block, hooks) + } else { + block.transition = hooks + } +} + +let registered = false +export function ensureVaporTransition(): void { + if (!registered) { + registerVaporTransition(vaporTransitionImpl) + registered = true + } +} diff --git a/packages/runtime-vapor/src/directives/vShow.ts b/packages/runtime-vapor/src/directives/vShow.ts index ac4c066b71..492d5225ef 100644 --- a/packages/runtime-vapor/src/directives/vShow.ts +++ b/packages/runtime-vapor/src/directives/vShow.ts @@ -39,13 +39,26 @@ function setDisplay(target: Block, value: unknown): void { if (target instanceof DynamicFragment) { return setDisplay(target.nodes, value) } + const { transition } = target if (target instanceof Element) { const el = target as VShowElement if (!(vShowOriginalDisplay in el)) { el[vShowOriginalDisplay] = el.style.display === 'none' ? '' : el.style.display } - el.style.display = value ? el[vShowOriginalDisplay]! : 'none' + if (transition) { + if (value) { + transition.beforeEnter(target) + el.style.display = el[vShowOriginalDisplay]! + transition.enter(target) + } else { + transition.leave(target, () => { + el.style.display = 'none' + }) + } + } else { + el.style.display = value ? el[vShowOriginalDisplay]! : 'none' + } el[vShowHidden] = !value } else if (__DEV__) { warn( diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 77228fd72a..efe8223c60 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -33,6 +33,7 @@ import type { RawSlots, VaporSlot } from './componentSlots' import { renderEffect } from './renderEffect' import { createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' +import { ensureVaporTransition } from './components/Transition' // mounting vapor components and slots in vdom const vaporInteropImpl: Omit< @@ -288,6 +289,7 @@ export const vaporInteropPlugin: Plugin = app => { const mount = app.mount app.mount = ((...args) => { optimizePropertyLookup() + ensureVaporTransition() return mount(...args) }) satisfies App['mount'] } -- 2.47.2