From 41822e3743eb68d927a14ae72a39bbf553d0bbb8 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 27 Feb 2025 16:41:33 +0800 Subject: [PATCH 01/62] 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 73e23150fa1..b131ad2e947 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 2b58bc3fc43..ae89f36356b 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 c7150e38e80..51f42562eeb 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 fcbfdd0426c..fc3664de8c7 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 6c6344bfcac..90cdaba4e73 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 51c72fe2ed1..0cfd08e87a2 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 8088e1aee6d..da09a79a12a 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 af188705594..a4cfcea1f9c 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 3c39612bb89..2c448bda246 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 00000000000..2eb87ebffe0 --- /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 ac4c066b71d..492d5225ef2 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 77228fd72a0..efe8223c604 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'] } From 7cee02438f585829590e2fbc45cde18f15dbcd63 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 27 Feb 2025 22:31:45 +0800 Subject: [PATCH 02/62] wip: handle mode --- .../src/components/BaseTransition.ts | 5 +- packages/runtime-vapor/src/block.ts | 61 +++++++++++++------ .../src/components/Transition.ts | 32 +++++++++- 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index ae89f36356b..673d30a777a 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -392,10 +392,11 @@ export function resolveTransitionHooks( if ( leavingVNode && isSameVNodeType(vnode, leavingVNode) && - (leavingVNode.el as TransitionElement)[leaveCbKey] + // TODO refactor + ((leavingVNode.el || leavingVNode) as TransitionElement)[leaveCbKey] ) { // force early removal (not cancelled) - ;(leavingVNode.el as TransitionElement)[leaveCbKey]!() + ;((leavingVNode.el || leavingVNode) as TransitionElement)[leaveCbKey]!() } callHook(hook, [el]) }, diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index a4cfcea1f9c..383c4e8e9fd 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -19,7 +19,16 @@ export type Block = ( | DynamicFragment | VaporComponentInstance | Block[] -) & { transition?: TransitionHooks } +) & + TransitionBlock + +export type TransitionBlock = { + transition?: TransitionHooks + applyLeavingHooks?: ( + block: Block, + afterLeaveCb: () => void, + ) => TransitionHooks +} export type BlockFn = (...args: any[]) => Block @@ -29,6 +38,10 @@ export class VaporFragment { insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void transition?: TransitionHooks + applyLeavingHooks?: ( + block: Block, + afterLeaveCb: () => void, + ) => TransitionHooks constructor(nodes: Block) { this.nodes = nodes @@ -56,29 +69,39 @@ export class DynamicFragment extends VaporFragment { pauseTracking() const parent = this.anchor.parentNode + const renderNewBranch = () => { + if (render) { + this.scope = new EffectScope() + this.nodes = this.scope.run(render) || [] + if (parent) insert(this.nodes, parent, this.anchor, this.transition) + } else { + this.scope = undefined + this.nodes = [] + } + + if (this.fallback && !isValidBlock(this.nodes)) { + parent && remove(this.nodes, parent, this.transition) + this.nodes = + (this.scope || (this.scope = new EffectScope())).run(this.fallback) || + [] + parent && insert(this.nodes, parent, this.anchor, this.transition) + } + } + // teardown previous branch if (this.scope) { this.scope.stop() - 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, this.transition) - } else { - this.scope = undefined - this.nodes = [] - } - - if (this.fallback && !isValidBlock(this.nodes)) { - parent && remove(this.nodes, parent) - this.nodes = - (this.scope || (this.scope = new EffectScope())).run(this.fallback) || - [] - parent && insert(this.nodes, parent, this.anchor, this.transition) + if (this.transition && this.transition.mode) { + const transition = this.applyLeavingHooks!(this.nodes, renderNewBranch) + parent && remove(this.nodes, parent, transition) + resetTracking() + return + } else { + parent && remove(this.nodes, parent, this.transition) + } } + renderNewBranch() resetTracking() } } diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 2eb87ebffe0..00cf52414e3 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -11,7 +11,10 @@ import type { Block } from '../block' import { isVaporComponent } from '../component' export const vaporTransitionImpl: VaporTransitionInterface = { - applyTransition: (props: TransitionProps, slots: { default: () => any }) => { + applyTransition: ( + props: TransitionProps, + slots: { default: () => Block }, + ) => { const children = slots.default && slots.default() if (!children) { return @@ -30,7 +33,32 @@ export const vaporTransitionImpl: VaporTransitionInterface = { ) setTransitionHooks(child, enterHooks) - // TODO handle mode + const { mode } = props + // TODO check mode + + child.applyLeavingHooks = (block: Block, afterLeaveCb: () => void) => { + let leavingHooks = resolveTransitionHooks( + block as any, + props, + state, + currentInstance!, + ) + setTransitionHooks(block, leavingHooks) + + if (mode === 'out-in') { + state.isLeaving = true + leavingHooks.afterLeave = () => { + state.isLeaving = false + afterLeaveCb() + delete leavingHooks.afterLeave + } + } else if (mode === 'in-out') { + leavingHooks.delayLeave = (block: Block, earlyRemove, delayedLeave) => { + // TODO delay leave + } + } + return leavingHooks + } return children }, From a8140ac826a15cfdf19af9b38ae1c6600dc60574 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 28 Feb 2025 09:45:04 +0800 Subject: [PATCH 03/62] refactor: reuse code from BaseTransition --- .../src/components/BaseTransition.ts | 95 +++++++++++++------ packages/runtime-core/src/index.ts | 4 + .../src/components/Transition.ts | 60 +++++++++++- 3 files changed, 129 insertions(+), 30 deletions(-) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 673d30a777a..c59fd7338b1 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -25,7 +25,7 @@ import { SchedulerJobFlags } from '../scheduler' type Hook void> = T | T[] -const leaveCbKey: unique symbol = Symbol('_leaveCb') +export const leaveCbKey: unique symbol = Symbol('_leaveCb') const enterCbKey: unique symbol = Symbol('_enterCb') export interface BaseTransitionProps { @@ -88,7 +88,7 @@ export interface TransitionState { isUnmounting: boolean // Track pending leave callbacks for children of the same key. // This is used to force remove leaving a child when a new copy is entering. - leavingVNodes: Map> + leavingVNodes: Map> } export interface TransitionElement { @@ -319,6 +319,13 @@ function getLeavingNodesForType( return leavingVNodesCache } +export interface TransitionHooksContext { + setLeavingNodeCache: () => void + unsetLeavingNodeCache: () => void + earlyRemove: () => void + cloneHooks: (node: any) => TransitionHooks +} + // The transition hooks are attached to the vnode as vnode.transition // and will be called at appropriate timing in the renderer. export function resolveTransitionHooks( @@ -328,6 +335,57 @@ export function resolveTransitionHooks( instance: GenericComponentInstance, postClone?: (hooks: TransitionHooks) => void, ): TransitionHooks { + const key = String(vnode.key) + const leavingVNodesCache = getLeavingNodesForType(state, vnode) + const context: TransitionHooksContext = { + setLeavingNodeCache: () => { + leavingVNodesCache[key] = vnode + }, + unsetLeavingNodeCache: () => { + if (leavingVNodesCache[key] === vnode) { + delete leavingVNodesCache[key] + } + }, + earlyRemove: () => { + const leavingVNode = leavingVNodesCache[key] + if ( + leavingVNode && + isSameVNodeType(vnode, leavingVNode) && + (leavingVNode.el as TransitionElement)[leaveCbKey] + ) { + // force early removal (not cancelled) + ;(leavingVNode.el as TransitionElement)[leaveCbKey]!() + } + }, + cloneHooks: vnode => { + const hooks = resolveTransitionHooks( + vnode, + props, + state, + instance, + postClone, + ) + if (postClone) postClone(hooks) + return hooks + }, + } + + return baseResolveTransitionHooks(context, props, state, instance) +} + +export function baseResolveTransitionHooks( + context: TransitionHooksContext, + props: BaseTransitionProps, + state: TransitionState, + instance: GenericComponentInstance, +): TransitionHooks { + const { + setLeavingNodeCache, + unsetLeavingNodeCache, + earlyRemove, + cloneHooks, + } = context + const { appear, mode, @@ -345,8 +403,6 @@ export function resolveTransitionHooks( onAfterAppear, onAppearCancelled, } = props - const key = String(vnode.key) - const leavingVNodesCache = getLeavingNodesForType(state, vnode) const callHook: TransitionHookCaller = (hook, args) => { hook && @@ -388,16 +444,7 @@ export function resolveTransitionHooks( el[leaveCbKey](true /* cancelled */) } // for toggled element with same key (v-if) - const leavingVNode = leavingVNodesCache[key] - if ( - leavingVNode && - isSameVNodeType(vnode, leavingVNode) && - // TODO refactor - ((leavingVNode.el || leavingVNode) as TransitionElement)[leaveCbKey] - ) { - // force early removal (not cancelled) - ;((leavingVNode.el || leavingVNode) as TransitionElement)[leaveCbKey]!() - } + earlyRemove() callHook(hook, [el]) }, @@ -436,7 +483,7 @@ export function resolveTransitionHooks( }, leave(el, remove) { - const key = String(vnode.key) + // const key = String(vnode.key) if (el[enterCbKey]) { el[enterCbKey](true /* cancelled */) } @@ -455,11 +502,9 @@ export function resolveTransitionHooks( callHook(onAfterLeave, [el]) } el[leaveCbKey] = undefined - if (leavingVNodesCache[key] === vnode) { - delete leavingVNodesCache[key] - } + unsetLeavingNodeCache() }) - leavingVNodesCache[key] = vnode + setLeavingNodeCache() if (onLeave) { callAsyncHook(onLeave, [el, done]) } else { @@ -467,16 +512,8 @@ export function resolveTransitionHooks( } }, - clone(vnode) { - const hooks = resolveTransitionHooks( - vnode, - props, - state, - instance, - postClone, - ) - if (postClone) postClone(hooks) - return hooks + clone(node) { + return cloneHooks(node) }, } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 51f42562eeb..9da2b2cba4b 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -150,8 +150,10 @@ export { registerRuntimeCompiler, isRuntimeOnly } from './component' export { useTransitionState, resolveTransitionHooks, + baseResolveTransitionHooks, setTransitionHooks, getTransitionRawChildren, + leaveCbKey, } from './components/BaseTransition' export { initCustomFormatter } from './customFormatter' @@ -335,6 +337,8 @@ export type { SuspenseBoundary } from './components/Suspense' export type { TransitionState, TransitionHooks, + TransitionHooksContext, + TransitionElement, } from './components/BaseTransition' export type { AsyncComponentOptions, diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 00cf52414e3..9b9410234b7 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -1,10 +1,15 @@ import { + type GenericComponentInstance, + type TransitionElement, type TransitionHooks, + type TransitionHooksContext, type TransitionProps, + type TransitionState, type VaporTransitionInterface, + baseResolveTransitionHooks, currentInstance, + leaveCbKey, registerVaporTransition, - resolveTransitionHooks, useTransitionState, } from '@vue/runtime-dom' import type { Block } from '../block' @@ -64,6 +69,59 @@ export const vaporTransitionImpl: VaporTransitionInterface = { }, } +function resolveTransitionHooks( + block: Block & { key: string }, + props: TransitionProps, + state: TransitionState, + instance: GenericComponentInstance, + postClone?: (hooks: TransitionHooks) => void, +): TransitionHooks { + const key = String(block.key) + const leavingNodeCache = getLeavingNodesForBlock(state, block) + const context: TransitionHooksContext = { + setLeavingNodeCache: () => { + leavingNodeCache[key] = block + }, + unsetLeavingNodeCache: () => { + if (leavingNodeCache[key] === block) { + delete leavingNodeCache[key] + } + }, + earlyRemove: () => { + const leavingNode = leavingNodeCache[key] + if (leavingNode && (leavingNode as TransitionElement)[leaveCbKey]) { + // force early removal (not cancelled) + ;(leavingNode as TransitionElement)[leaveCbKey]!() + } + }, + cloneHooks: block => { + const hooks = resolveTransitionHooks( + block, + props, + state, + instance, + postClone, + ) + if (postClone) postClone(hooks) + return hooks + }, + } + return baseResolveTransitionHooks(context, props, state, instance) +} + +function getLeavingNodesForBlock( + state: TransitionState, + block: Block, +): Record { + const { leavingVNodes } = state + let leavingNodesCache = leavingVNodes.get(block)! + if (!leavingNodesCache) { + leavingNodesCache = Object.create(null) + leavingVNodes.set(block, leavingNodesCache) + } + return leavingNodesCache +} + function setTransitionHooks(block: Block, hooks: TransitionHooks) { if (isVaporComponent(block)) { setTransitionHooks(block.block, hooks) From 8957eaa3197cc9de3caffb511a094cd08660fe75 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 28 Feb 2025 16:19:03 +0800 Subject: [PATCH 04/62] wip: handle in-out mode --- packages/runtime-vapor/src/block.ts | 10 +++++-- .../src/components/Transition.ts | 30 +++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 383c4e8e9fd..e0748fe879b 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -23,6 +23,7 @@ export type Block = ( TransitionBlock export type TransitionBlock = { + key?: any transition?: TransitionHooks applyLeavingHooks?: ( block: Block, @@ -91,11 +92,14 @@ export class DynamicFragment extends VaporFragment { // teardown previous branch if (this.scope) { this.scope.stop() - if (this.transition && this.transition.mode) { + const mode = this.transition && this.transition.mode + if (mode) { const transition = this.applyLeavingHooks!(this.nodes, renderNewBranch) parent && remove(this.nodes, parent, transition) - resetTracking() - return + if (mode === 'out-in') { + resetTracking() + return + } } else { parent && remove(this.nodes, parent, this.transition) } diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 9b9410234b7..b203db46698 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -41,14 +41,17 @@ export const vaporTransitionImpl: VaporTransitionInterface = { const { mode } = props // TODO check mode - child.applyLeavingHooks = (block: Block, afterLeaveCb: () => void) => { + child.applyLeavingHooks = ( + leavingBlock: Block, + afterLeaveCb: () => void, + ) => { let leavingHooks = resolveTransitionHooks( - block as any, + leavingBlock as any, props, state, currentInstance!, ) - setTransitionHooks(block, leavingHooks) + setTransitionHooks(leavingBlock, leavingHooks) if (mode === 'out-in') { state.isLeaving = true @@ -58,8 +61,23 @@ export const vaporTransitionImpl: VaporTransitionInterface = { delete leavingHooks.afterLeave } } else if (mode === 'in-out') { - leavingHooks.delayLeave = (block: Block, earlyRemove, delayedLeave) => { - // TODO delay leave + leavingHooks.delayLeave = ( + block: TransitionElement, + earlyRemove, + delayedLeave, + ) => { + const leavingNodeCache = getLeavingNodesForBlock(state, leavingBlock) + leavingNodeCache[String(leavingBlock.key)] = leavingBlock + // early removal callback + block[leaveCbKey] = () => { + earlyRemove() + block[leaveCbKey] = undefined + delete enterHooks.delayedLeave + } + enterHooks.delayedLeave = () => { + delayedLeave() + delete enterHooks.delayedLeave + } } } return leavingHooks @@ -70,7 +88,7 @@ export const vaporTransitionImpl: VaporTransitionInterface = { } function resolveTransitionHooks( - block: Block & { key: string }, + block: Block, props: TransitionProps, state: TransitionState, instance: GenericComponentInstance, From 413651d6e6085e38650539f0a871323da452e907 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 3 Mar 2025 11:44:57 +0800 Subject: [PATCH 05/62] wip: save --- packages/runtime-vapor/src/block.ts | 39 ++++---- .../src/components/Transition.ts | 99 ++++++++++++++++--- 2 files changed, 105 insertions(+), 33 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index e0748fe879b..8b62dad1308 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -25,10 +25,10 @@ export type Block = ( export type TransitionBlock = { key?: any transition?: TransitionHooks - applyLeavingHooks?: ( + applyTransitionLeavingHooks?: ( block: Block, afterLeaveCb: () => void, - ) => TransitionHooks + ) => TransitionHooks | undefined } export type BlockFn = (...args: any[]) => Block @@ -39,10 +39,10 @@ export class VaporFragment { insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void transition?: TransitionHooks - applyLeavingHooks?: ( + applyTransitionLeavingHooks?: ( block: Block, afterLeaveCb: () => void, - ) => TransitionHooks + ) => TransitionHooks | undefined constructor(nodes: Block) { this.nodes = nodes @@ -70,7 +70,7 @@ export class DynamicFragment extends VaporFragment { pauseTracking() const parent = this.anchor.parentNode - const renderNewBranch = () => { + const renderBranch = () => { if (render) { this.scope = new EffectScope() this.nodes = this.scope.run(render) || [] @@ -79,14 +79,6 @@ export class DynamicFragment extends VaporFragment { this.scope = undefined this.nodes = [] } - - if (this.fallback && !isValidBlock(this.nodes)) { - parent && remove(this.nodes, parent, this.transition) - this.nodes = - (this.scope || (this.scope = new EffectScope())).run(this.fallback) || - [] - parent && insert(this.nodes, parent, this.anchor, this.transition) - } } // teardown previous branch @@ -94,7 +86,10 @@ export class DynamicFragment extends VaporFragment { this.scope.stop() const mode = this.transition && this.transition.mode if (mode) { - const transition = this.applyLeavingHooks!(this.nodes, renderNewBranch) + const transition = this.applyTransitionLeavingHooks!( + this.nodes, + renderBranch, + ) parent && remove(this.nodes, parent, transition) if (mode === 'out-in') { resetTracking() @@ -105,7 +100,16 @@ export class DynamicFragment extends VaporFragment { } } - renderNewBranch() + renderBranch() + + if (this.fallback && !isValidBlock(this.nodes)) { + parent && remove(this.nodes, parent, this.transition) + this.nodes = + (this.scope || (this.scope = new EffectScope())).run(this.fallback) || + [] + parent && insert(this.nodes, parent, this.anchor, this.transition) + } + resetTracking() } } @@ -145,7 +149,8 @@ export function insert( ): void { anchor = anchor === 0 ? parent.firstChild : anchor if (block instanceof Node) { - if (transition) { + // don't apply transition on text or comment nodes + if (transition && block instanceof Element) { applyTransitionEnter( block, transition, @@ -183,7 +188,7 @@ export function remove( transition: TransitionHooks | undefined = block.transition, ): void { if (block instanceof Node) { - if (transition) { + if (transition && block instanceof Element) { applyTransitionLeave( block, transition, diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index b203db46698..28c6cd0a7f0 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -11,6 +11,7 @@ import { leaveCbKey, registerVaporTransition, useTransitionState, + warn, } from '@vue/runtime-dom' import type { Block } from '../block' import { isVaporComponent } from '../component' @@ -21,12 +22,10 @@ export const vaporTransitionImpl: VaporTransitionInterface = { slots: { default: () => Block }, ) => { const children = slots.default && slots.default() - if (!children) { - return - } + if (!children) return - // TODO find non-comment node - const child = children + const child = findElementChild(children) + if (!child) return const state = useTransitionState() let enterHooks = resolveTransitionHooks( @@ -39,12 +38,23 @@ export const vaporTransitionImpl: VaporTransitionInterface = { setTransitionHooks(child, enterHooks) const { mode } = props - // TODO check mode + if ( + __DEV__ && + mode && + mode !== 'in-out' && + mode !== 'out-in' && + mode !== 'default' + ) { + warn(`invalid mode: ${mode}`) + } - child.applyLeavingHooks = ( - leavingBlock: Block, + child.applyTransitionLeavingHooks = ( + block: Block, afterLeaveCb: () => void, ) => { + const leavingBlock = findElementChild(block) + if (!leavingBlock) return undefined + let leavingHooks = resolveTransitionHooks( leavingBlock as any, props, @@ -87,15 +97,15 @@ export const vaporTransitionImpl: VaporTransitionInterface = { }, } -function resolveTransitionHooks( +const getTransitionHooksContext = ( + leavingNodeCache: Record, + key: string, block: Block, props: TransitionProps, state: TransitionState, instance: GenericComponentInstance, - postClone?: (hooks: TransitionHooks) => void, -): TransitionHooks { - const key = String(block.key) - const leavingNodeCache = getLeavingNodesForBlock(state, block) + postClone: ((hooks: TransitionHooks) => void) | undefined, +) => { const context: TransitionHooksContext = { setLeavingNodeCache: () => { leavingNodeCache[key] = block @@ -124,6 +134,27 @@ function resolveTransitionHooks( return hooks }, } + return context +} + +function resolveTransitionHooks( + block: Block, + props: TransitionProps, + state: TransitionState, + instance: GenericComponentInstance, + postClone?: (hooks: TransitionHooks) => void, +): TransitionHooks { + const key = String(block.key) + const leavingNodeCache = getLeavingNodesForBlock(state, block) + const context = getTransitionHooksContext( + leavingNodeCache, + key, + block, + props, + state, + instance, + postClone, + ) return baseResolveTransitionHooks(context, props, state, instance) } @@ -141,11 +172,47 @@ function getLeavingNodesForBlock( } function setTransitionHooks(block: Block, hooks: TransitionHooks) { - if (isVaporComponent(block)) { - setTransitionHooks(block.block, hooks) + block.transition = hooks +} + +function findElementChild(block: Block): Block | undefined { + let child: Block | undefined + // transition can only be applied on Element child + if (block instanceof Element) { + child = block + } else if (isVaporComponent(block)) { + child = findElementChild(block.block) + } else if (Array.isArray(block)) { + child = block[0] + let hasFound = false + for (const c of block) { + const item = findElementChild(c) + if (item instanceof Element) { + if (__DEV__ && hasFound) { + // warn more than one non-comment child + warn( + ' can only be used on a single element or component. ' + + 'Use for lists.', + ) + break + } + child = item + hasFound = true + if (!__DEV__) break + } + } } else { - block.transition = hooks + // fragment + // store transition hooks on fragment itself, so it can apply to both + // previous and new branch during updates. + child = block } + + if (__DEV__ && !child) { + warn('Transition component has no valid child element') + } + + return child } let registered = false From 1e7905408a873aac496b57772365fdd843f1438e Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 3 Mar 2025 21:58:09 +0800 Subject: [PATCH 06/62] wip: save --- .../src/components/BaseTransition.ts | 8 +- packages/runtime-core/src/index.ts | 2 +- packages/runtime-core/src/renderer.ts | 10 +- packages/runtime-vapor/src/block.ts | 85 ++++---- packages/runtime-vapor/src/component.ts | 9 +- .../src/components/Transition.ts | 189 +++++++++++------- 6 files changed, 177 insertions(+), 126 deletions(-) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index c59fd7338b1..7df3d349bf5 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -320,8 +320,8 @@ function getLeavingNodesForType( } export interface TransitionHooksContext { - setLeavingNodeCache: () => void - unsetLeavingNodeCache: () => void + setLeavingNodeCache: (node: any) => void + unsetLeavingNodeCache: (node: any) => void earlyRemove: () => void cloneHooks: (node: any) => TransitionHooks } @@ -502,9 +502,9 @@ export function baseResolveTransitionHooks( callHook(onAfterLeave, [el]) } el[leaveCbKey] = undefined - unsetLeavingNodeCache() + unsetLeavingNodeCache(el) }) - setLeavingNodeCache() + setLeavingNodeCache(el) if (onLeave) { callAsyncHook(onLeave, [el, done]) } else { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 9da2b2cba4b..596c31e5883 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -564,4 +564,4 @@ export { initFeatureFlags } from './featureFlags' /** * @internal */ -export { applyTransitionEnter, applyTransitionLeave } from './renderer' +export { performTransitionEnter, performTransitionLeave } from './renderer' diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index fc3664de8c7..5750bc16891 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -732,7 +732,7 @@ 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 if (transition) { - applyTransitionEnter( + performTransitionEnter( el, transition, () => hostInsert(el, container, anchor), @@ -2116,7 +2116,7 @@ function baseCreateRenderer( transition if (needTransition) { if (moveType === MoveType.ENTER) { - applyTransitionEnter( + performTransitionEnter( el!, transition, () => hostInsert(el!, container, anchor), @@ -2297,7 +2297,7 @@ function baseCreateRenderer( } if (transition) { - applyTransitionLeave( + performTransitionLeave( el!, transition, () => hostRemove(el!), @@ -2622,7 +2622,7 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void { } } -export function applyTransitionEnter( +export function performTransitionEnter( el: RendererElement, transition: TransitionHooks, insert: () => void, @@ -2637,7 +2637,7 @@ export function applyTransitionEnter( } } -export function applyTransitionLeave( +export function performTransitionLeave( el: RendererElement, transition: TransitionHooks, remove: () => void, diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 8b62dad1308..d3019b80f0f 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -9,9 +9,15 @@ import { createComment, createTextNode } from './dom/node' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { type TransitionHooks, - applyTransitionEnter, - applyTransitionLeave, + type TransitionProps, + type TransitionState, + performTransitionEnter, + performTransitionLeave, } from '@vue/runtime-dom' +import { + applyTransitionEnterHooks, + applyTransitionLeaveHooks, +} from './components/Transition' export type Block = ( | Node @@ -22,13 +28,14 @@ export type Block = ( ) & TransitionBlock +export interface VaporTransitionHooks extends TransitionHooks { + state?: TransitionState + props?: TransitionProps +} + export type TransitionBlock = { key?: any - transition?: TransitionHooks - applyTransitionLeavingHooks?: ( - block: Block, - afterLeaveCb: () => void, - ) => TransitionHooks | undefined + transition?: VaporTransitionHooks } export type BlockFn = (...args: any[]) => Block @@ -38,11 +45,7 @@ export class VaporFragment { anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void - transition?: TransitionHooks - applyTransitionLeavingHooks?: ( - block: Block, - afterLeaveCb: () => void, - ) => TransitionHooks | undefined + transitionChild?: TransitionBlock | undefined constructor(nodes: Block) { this.nodes = nodes @@ -54,6 +57,7 @@ export class DynamicFragment extends VaporFragment { scope: EffectScope | undefined current?: BlockFn fallback?: BlockFn + transitionChild?: Block constructor(anchorLabel?: string) { super([]) @@ -72,9 +76,18 @@ export class DynamicFragment extends VaporFragment { const renderBranch = () => { if (render) { + const transition = this.transition this.scope = new EffectScope() this.nodes = this.scope.run(render) || [] - if (parent) insert(this.nodes, parent, this.anchor, this.transition) + if (transition) { + this.transitionChild = applyTransitionEnterHooks( + this.nodes, + transition.state!, + transition.props!, + transition, + ) + } + if (parent) insert(this.nodes, parent, this.anchor) } else { this.scope = undefined this.nodes = [] @@ -86,32 +99,39 @@ export class DynamicFragment extends VaporFragment { this.scope.stop() const mode = this.transition && this.transition.mode if (mode) { - const transition = this.applyTransitionLeavingHooks!( + applyTransitionLeaveHooks( this.nodes, + this.transition!.state!, + this.transition!.props!, renderBranch, + this.transition, ) - parent && remove(this.nodes, parent, transition) + parent && remove(this.nodes, parent) if (mode === 'out-in') { resetTracking() return } } else { - parent && remove(this.nodes, parent, this.transition) + parent && remove(this.nodes, parent) } } renderBranch() if (this.fallback && !isValidBlock(this.nodes)) { - parent && remove(this.nodes, parent, this.transition) + parent && remove(this.nodes, parent) this.nodes = (this.scope || (this.scope = new EffectScope())).run(this.fallback) || [] - parent && insert(this.nodes, parent, this.anchor, this.transition) + parent && insert(this.nodes, parent, this.anchor) } resetTracking() } + + get transition(): VaporTransitionHooks | undefined { + return this.transitionChild && this.transitionChild.transition + } } export function isFragment(val: NonNullable): val is VaporFragment { @@ -144,16 +164,16 @@ 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) { // don't apply transition on text or comment nodes - if (transition && block instanceof Element) { - applyTransitionEnter( + if (block.transition && block instanceof Element) { + performTransitionEnter( block, - transition, + // @ts-expect-error + block.transition, () => parent.insertBefore(block, anchor), parentSuspense, ) @@ -161,7 +181,7 @@ export function insert( parent.insertBefore(block, anchor) } } else if (isVaporComponent(block)) { - mountComponent(block, parent, anchor, transition) + mountComponent(block, parent, anchor) } else if (isArray(block)) { for (let i = 0; i < block.length; i++) { insert(block[i], parent, anchor) @@ -171,7 +191,7 @@ export function insert( if (block.insert) { block.insert(parent, anchor) } else { - insert(block.nodes, parent, anchor, block.transition) + insert(block.nodes, parent, anchor, parentSuspense) } if (block.anchor) insert(block.anchor, parent, anchor) } @@ -182,23 +202,20 @@ export function prepend(parent: ParentNode, ...blocks: Block[]): void { while (i--) insert(blocks[i], parent, 0) } -export function remove( - block: Block, - parent?: ParentNode, - transition: TransitionHooks | undefined = block.transition, -): void { +export function remove(block: Block, parent?: ParentNode): void { if (block instanceof Node) { - if (transition && block instanceof Element) { - applyTransitionLeave( + if (block.transition && block instanceof Element) { + performTransitionLeave( block, - transition, + // @ts-expect-error + block.transition, () => parent && parent.removeChild(block), ) } else { parent && parent.removeChild(block) } } else if (isVaporComponent(block)) { - unmountComponent(block, parent, transition) + unmountComponent(block, parent) } else if (isArray(block)) { for (let i = 0; i < block.length; i++) { remove(block[i], parent) @@ -208,7 +225,7 @@ export function remove( if (block.remove) { block.remove(parent) } else { - remove(block.nodes, parent, block.transition) + remove(block.nodes, parent) } 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 2c448bda246..3c39612bb89 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -11,7 +11,6 @@ import { type NormalizedPropsOptions, type ObjectEmitsOptions, type SuspenseBoundary, - type TransitionHooks, callWithErrorHandling, currentInstance, endMeasure, @@ -476,18 +475,17 @@ 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, transition) + insert(instance.block, parent, anchor) if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!)) instance.isMounted = true } else { - insert(instance.block, parent, anchor, transition) + insert(instance.block, parent, anchor) } if (__DEV__) { endMeasure(instance, `mount`) @@ -497,7 +495,6 @@ export function mountComponent( export function unmountComponent( instance: VaporComponentInstance, parentNode?: ParentNode, - transition?: TransitionHooks, ): void { if (instance.isMounted && !instance.isUnmounted) { if (__DEV__ && instance.type.__hmrId) { @@ -516,7 +513,7 @@ export function unmountComponent( } if (parentNode) { - remove(instance.block, parentNode, transition) + remove(instance.block, parentNode) } } diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 28c6cd0a7f0..cae810067b0 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -13,7 +13,7 @@ import { useTransitionState, warn, } from '@vue/runtime-dom' -import type { Block } from '../block' +import { type Block, type VaporTransitionHooks, isFragment } from '../block' import { isVaporComponent } from '../component' export const vaporTransitionImpl: VaporTransitionInterface = { @@ -24,19 +24,6 @@ export const vaporTransitionImpl: VaporTransitionInterface = { const children = slots.default && slots.default() if (!children) return - const child = findElementChild(children) - if (!child) return - - const state = useTransitionState() - let enterHooks = resolveTransitionHooks( - child as any, - props, - state, - currentInstance!, - hooks => (enterHooks = hooks), - ) - setTransitionHooks(child, enterHooks) - const { mode } = props if ( __DEV__ && @@ -48,57 +35,19 @@ export const vaporTransitionImpl: VaporTransitionInterface = { warn(`invalid mode: ${mode}`) } - child.applyTransitionLeavingHooks = ( - block: Block, - afterLeaveCb: () => void, - ) => { - const leavingBlock = findElementChild(block) - if (!leavingBlock) return undefined - - let leavingHooks = resolveTransitionHooks( - leavingBlock as any, - props, - state, - currentInstance!, - ) - setTransitionHooks(leavingBlock, leavingHooks) - - if (mode === 'out-in') { - state.isLeaving = true - leavingHooks.afterLeave = () => { - state.isLeaving = false - afterLeaveCb() - delete leavingHooks.afterLeave - } - } else if (mode === 'in-out') { - leavingHooks.delayLeave = ( - block: TransitionElement, - earlyRemove, - delayedLeave, - ) => { - const leavingNodeCache = getLeavingNodesForBlock(state, leavingBlock) - leavingNodeCache[String(leavingBlock.key)] = leavingBlock - // early removal callback - block[leaveCbKey] = () => { - earlyRemove() - block[leaveCbKey] = undefined - delete enterHooks.delayedLeave - } - enterHooks.delayedLeave = () => { - delayedLeave() - delete enterHooks.delayedLeave - } - } - } - return leavingHooks - } + applyTransitionEnterHooks( + children, + useTransitionState(), + props, + undefined, + false, + ) return children }, } const getTransitionHooksContext = ( - leavingNodeCache: Record, key: string, block: Block, props: TransitionProps, @@ -107,15 +56,18 @@ const getTransitionHooksContext = ( postClone: ((hooks: TransitionHooks) => void) | undefined, ) => { const context: TransitionHooksContext = { - setLeavingNodeCache: () => { - leavingNodeCache[key] = block + setLeavingNodeCache: el => { + const leavingNodeCache = getLeavingNodesForBlock(state, block) + leavingNodeCache[key] = el }, - unsetLeavingNodeCache: () => { - if (leavingNodeCache[key] === block) { + unsetLeavingNodeCache: el => { + const leavingNodeCache = getLeavingNodesForBlock(state, block) + if (leavingNodeCache[key] === el) { delete leavingNodeCache[key] } }, earlyRemove: () => { + const leavingNodeCache = getLeavingNodesForBlock(state, block) const leavingNode = leavingNodeCache[key] if (leavingNode && (leavingNode as TransitionElement)[leaveCbKey]) { // force early removal (not cancelled) @@ -143,11 +95,9 @@ function resolveTransitionHooks( state: TransitionState, instance: GenericComponentInstance, postClone?: (hooks: TransitionHooks) => void, -): TransitionHooks { +): VaporTransitionHooks { const key = String(block.key) - const leavingNodeCache = getLeavingNodesForBlock(state, block) const context = getTransitionHooksContext( - leavingNodeCache, key, block, props, @@ -155,7 +105,15 @@ function resolveTransitionHooks( instance, postClone, ) - return baseResolveTransitionHooks(context, props, state, instance) + const hooks: VaporTransitionHooks = baseResolveTransitionHooks( + context, + props, + state, + instance, + ) + hooks.state = state + hooks.props = props + return hooks } function getLeavingNodesForBlock( @@ -171,15 +129,96 @@ function getLeavingNodesForBlock( return leavingNodesCache } -function setTransitionHooks(block: Block, hooks: TransitionHooks) { - block.transition = hooks +function setTransitionHooks(block: Block, hooks: VaporTransitionHooks) { + if (!isFragment(block)) { + block.transition = hooks + } +} + +export function applyTransitionEnterHooks( + block: Block, + state: TransitionState, + props: TransitionProps, + enterHooks?: VaporTransitionHooks, + clone: boolean = true, +): Block | undefined { + const child = findElementChild(block) + if (child) { + if (!enterHooks) { + enterHooks = resolveTransitionHooks( + child, + props, + state, + currentInstance!, + hooks => (enterHooks = hooks), + ) + } + + setTransitionHooks( + child, + clone ? enterHooks.clone(child as any) : enterHooks, + ) + + if (isFragment(block)) { + block.transitionChild = child + } + } + return child +} + +export function applyTransitionLeaveHooks( + block: Block, + state: TransitionState, + props: TransitionProps, + afterLeaveCb: () => void, + enterHooks: TransitionHooks, +): void { + const leavingBlock = findElementChild(block) + if (!leavingBlock) return undefined + + let leavingHooks = resolveTransitionHooks( + leavingBlock, + props, + state, + currentInstance!, + ) + setTransitionHooks(leavingBlock, leavingHooks) + + const { mode } = props + if (mode === 'out-in') { + state.isLeaving = true + leavingHooks.afterLeave = () => { + state.isLeaving = false + afterLeaveCb() + delete leavingHooks.afterLeave + } + } else if (mode === 'in-out') { + leavingHooks.delayLeave = ( + block: TransitionElement, + earlyRemove, + delayedLeave, + ) => { + const leavingNodeCache = getLeavingNodesForBlock(state, leavingBlock) + leavingNodeCache[String(leavingBlock.key)] = leavingBlock + // early removal callback + block[leaveCbKey] = () => { + earlyRemove() + block[leaveCbKey] = undefined + delete enterHooks.delayedLeave + } + enterHooks.delayedLeave = () => { + delayedLeave() + delete enterHooks.delayedLeave + } + } + } } -function findElementChild(block: Block): Block | undefined { +export function findElementChild(block: Block): Block | undefined { let child: Block | undefined - // transition can only be applied on Element child - if (block instanceof Element) { - child = block + if (block instanceof Node) { + // transition can only be applied on Element child + if (block instanceof Element) child = block } else if (isVaporComponent(block)) { child = findElementChild(block.block) } else if (Array.isArray(block)) { @@ -203,9 +242,7 @@ function findElementChild(block: Block): Block | undefined { } } else { // fragment - // store transition hooks on fragment itself, so it can apply to both - // previous and new branch during updates. - child = block + child = findElementChild(block.nodes) } if (__DEV__ && !child) { From 75de3bb9ff2fdfcd265c0d590bf9f93c9f925a15 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 4 Mar 2025 10:34:28 +0800 Subject: [PATCH 07/62] wip: save --- .../src/components/BaseTransition.ts | 10 +-- packages/runtime-vapor/src/block.ts | 18 ++-- .../src/components/Transition.ts | 82 ++++++------------- 3 files changed, 36 insertions(+), 74 deletions(-) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 7df3d349bf5..5f522b5cf6e 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -88,7 +88,7 @@ export interface TransitionState { isUnmounting: boolean // Track pending leave callbacks for children of the same key. // This is used to force remove leaving a child when a new copy is entering. - leavingVNodes: Map> + leavingNodes: Map> } export interface TransitionElement { @@ -104,7 +104,7 @@ export function useTransitionState(): TransitionState { isMounted: false, isLeaving: false, isUnmounting: false, - leavingVNodes: new Map(), + leavingNodes: new Map(), } onMounted(() => { state.isMounted = true @@ -310,11 +310,11 @@ function getLeavingNodesForType( state: TransitionState, vnode: VNode, ): Record { - const { leavingVNodes } = state - let leavingVNodesCache = leavingVNodes.get(vnode.type)! + const { leavingNodes } = state + let leavingVNodesCache = leavingNodes.get(vnode.type)! if (!leavingVNodesCache) { leavingVNodesCache = Object.create(null) - leavingVNodes.set(vnode.type, leavingVNodesCache) + leavingNodes.set(vnode.type, leavingVNodesCache) } return leavingVNodesCache } diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index d3019b80f0f..96c4d94d423 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -29,8 +29,8 @@ export type Block = ( TransitionBlock export interface VaporTransitionHooks extends TransitionHooks { - state?: TransitionState - props?: TransitionProps + state: TransitionState + props: TransitionProps } export type TransitionBlock = { @@ -74,16 +74,14 @@ export class DynamicFragment extends VaporFragment { pauseTracking() const parent = this.anchor.parentNode + const transition = this.transition const renderBranch = () => { if (render) { - const transition = this.transition this.scope = new EffectScope() this.nodes = this.scope.run(render) || [] if (transition) { this.transitionChild = applyTransitionEnterHooks( this.nodes, - transition.state!, - transition.props!, transition, ) } @@ -97,15 +95,9 @@ export class DynamicFragment extends VaporFragment { // teardown previous branch if (this.scope) { this.scope.stop() - const mode = this.transition && this.transition.mode + const mode = transition && transition.mode if (mode) { - applyTransitionLeaveHooks( - this.nodes, - this.transition!.state!, - this.transition!.props!, - renderBranch, - this.transition, - ) + applyTransitionLeaveHooks(this.nodes, transition, renderBranch) parent && remove(this.nodes, parent) if (mode === 'out-in') { resetTracking() diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index cae810067b0..210c0855ef6 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -35,40 +35,35 @@ export const vaporTransitionImpl: VaporTransitionInterface = { warn(`invalid mode: ${mode}`) } - applyTransitionEnterHooks( - children, - useTransitionState(), + applyTransitionEnterHooks(children, { + state: useTransitionState(), props, - undefined, - false, - ) + } as VaporTransitionHooks) return children }, } const getTransitionHooksContext = ( - key: string, - block: Block, + key: String, props: TransitionProps, state: TransitionState, instance: GenericComponentInstance, postClone: ((hooks: TransitionHooks) => void) | undefined, ) => { + const { leavingNodes } = state const context: TransitionHooksContext = { setLeavingNodeCache: el => { - const leavingNodeCache = getLeavingNodesForBlock(state, block) - leavingNodeCache[key] = el + leavingNodes.set(key, el) }, unsetLeavingNodeCache: el => { - const leavingNodeCache = getLeavingNodesForBlock(state, block) - if (leavingNodeCache[key] === el) { - delete leavingNodeCache[key] + const leavingNode = leavingNodes.get(key) + if (leavingNode === el) { + leavingNodes.delete(key) } }, earlyRemove: () => { - const leavingNodeCache = getLeavingNodesForBlock(state, block) - const leavingNode = leavingNodeCache[key] + const leavingNode = leavingNodes.get(key) if (leavingNode && (leavingNode as TransitionElement)[leaveCbKey]) { // force early removal (not cancelled) ;(leavingNode as TransitionElement)[leaveCbKey]!() @@ -96,39 +91,24 @@ function resolveTransitionHooks( instance: GenericComponentInstance, postClone?: (hooks: TransitionHooks) => void, ): VaporTransitionHooks { - const key = String(block.key) const context = getTransitionHooksContext( - key, - block, + String(block.key), props, state, instance, postClone, ) - const hooks: VaporTransitionHooks = baseResolveTransitionHooks( + const hooks = baseResolveTransitionHooks( context, props, state, instance, - ) + ) as VaporTransitionHooks hooks.state = state hooks.props = props return hooks } -function getLeavingNodesForBlock( - state: TransitionState, - block: Block, -): Record { - const { leavingVNodes } = state - let leavingNodesCache = leavingVNodes.get(block)! - if (!leavingNodesCache) { - leavingNodesCache = Object.create(null) - leavingVNodes.set(block, leavingNodesCache) - } - return leavingNodesCache -} - function setTransitionHooks(block: Block, hooks: VaporTransitionHooks) { if (!isFragment(block)) { block.transition = hooks @@ -137,28 +117,20 @@ function setTransitionHooks(block: Block, hooks: VaporTransitionHooks) { export function applyTransitionEnterHooks( block: Block, - state: TransitionState, - props: TransitionProps, - enterHooks?: VaporTransitionHooks, - clone: boolean = true, + hooks: VaporTransitionHooks, ): Block | undefined { const child = findElementChild(block) if (child) { - if (!enterHooks) { - enterHooks = resolveTransitionHooks( - child, - props, - state, - currentInstance!, - hooks => (enterHooks = hooks), - ) - } - - setTransitionHooks( + const { props, state, delayedLeave } = hooks + let enterHooks = resolveTransitionHooks( child, - clone ? enterHooks.clone(child as any) : enterHooks, + props, + state, + currentInstance!, + hooks => (enterHooks = hooks as VaporTransitionHooks), ) - + enterHooks.delayedLeave = delayedLeave + setTransitionHooks(child, enterHooks) if (isFragment(block)) { block.transitionChild = child } @@ -168,15 +140,14 @@ export function applyTransitionEnterHooks( export function applyTransitionLeaveHooks( block: Block, - state: TransitionState, - props: TransitionProps, + enterHooks: VaporTransitionHooks, afterLeaveCb: () => void, - enterHooks: TransitionHooks, ): void { const leavingBlock = findElementChild(block) if (!leavingBlock) return undefined - let leavingHooks = resolveTransitionHooks( + const { props, state } = enterHooks + const leavingHooks = resolveTransitionHooks( leavingBlock, props, state, @@ -198,8 +169,7 @@ export function applyTransitionLeaveHooks( earlyRemove, delayedLeave, ) => { - const leavingNodeCache = getLeavingNodesForBlock(state, leavingBlock) - leavingNodeCache[String(leavingBlock.key)] = leavingBlock + state.leavingNodes.set(String(leavingBlock.key), leavingBlock) // early removal callback block[leaveCbKey] = () => { earlyRemove() From 3a31f0845e6ce112dae6ada27aa28ce4ae2550c4 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 4 Mar 2025 14:20:51 +0800 Subject: [PATCH 08/62] wip: auto generate key for vif branch if it wraps in transition --- packages/compiler-vapor/src/generators/block.ts | 6 ++++++ packages/compiler-vapor/src/ir/index.ts | 1 + packages/compiler-vapor/src/transforms/vIf.ts | 13 +++++++++++++ 3 files changed, 20 insertions(+) diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts index 77ba4bee8a7..4e568a91e7f 100644 --- a/packages/compiler-vapor/src/generators/block.ts +++ b/packages/compiler-vapor/src/generators/block.ts @@ -55,6 +55,12 @@ export function genBlockContent( push(...genOperations(operation, context)) push(...genEffects(effect, context)) + if (dynamic.needsKey) { + for (const child of dynamic.children) { + push(NEWLINE, `n${child.id}.key = ${JSON.stringify(child.id)}`) + } + } + push(NEWLINE, `return `) const returnNodes = returns.map(n => `n${n}`) diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 1509d37424c..b048e40d79b 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -259,6 +259,7 @@ export interface IRDynamicInfo { children: IRDynamicInfo[] template?: number hasDynamicChild?: boolean + needsKey?: boolean } export interface IREffect { diff --git a/packages/compiler-vapor/src/transforms/vIf.ts b/packages/compiler-vapor/src/transforms/vIf.ts index 8fad9c3146e..bf9b178fd34 100644 --- a/packages/compiler-vapor/src/transforms/vIf.ts +++ b/packages/compiler-vapor/src/transforms/vIf.ts @@ -1,6 +1,7 @@ import { type ElementNode, ErrorCodes, + NodeTypes, createCompilerError, createSimpleExpression, } from '@vue/compiler-dom' @@ -123,5 +124,17 @@ export function createIfBranch( const branch: BlockIRNode = newBlock(node) const exitBlock = context.enterBlock(branch) context.reference() + // generate key for branch result when it's in transition + // the key will be used to cache node at runtime + branch.dynamic.needsKey = isInTransition(context) return [branch, exitBlock] } + +function isInTransition(context: TransformContext): boolean { + const parentNode = context.parent && context.parent.node + return !!( + parentNode && + parentNode.type === NodeTypes.ELEMENT && + (parentNode.tag === 'transition' || parentNode.tag === 'Transition') + ) +} From 7e593c26589c5b8008a90b60ba330a88556c9444 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 4 Mar 2025 15:12:02 +0800 Subject: [PATCH 09/62] wip: handle built-in components --- packages/compiler-vapor/src/generators/component.ts | 8 ++++++++ .../src/transforms/transformElement.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index b131ad2e947..8b0ab73e78f 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -39,6 +39,7 @@ import { genEventHandler } from './event' import { genDirectiveModifiers, genDirectivesForElement } from './directive' import { genBlock } from './block' import { genModelHandler } from './vModel' +import { isBuiltInComponent } from '../transforms/transformElement' export function genCreateComponent( operation: CreateComponentIRNode, @@ -90,6 +91,13 @@ export function genCreateComponent( } else if (operation.asset) { return toValidAssetId(operation.tag, 'component') } else { + const { tag } = operation + const builtInTag = isBuiltInComponent(tag) + if (builtInTag) { + // @ts-expect-error + helper(builtInTag) + return `_${builtInTag}` + } return genExpression( extend(createSimpleExpression(operation.tag, false), { ast: null }), context, diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index f42801ace6d..14cdf66c2ee 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -109,6 +109,12 @@ function transformComponentElement( asset = false } + const builtInTag = isBuiltInComponent(tag) + if (builtInTag) { + tag = builtInTag + asset = false + } + const dotIndex = tag.indexOf('.') if (dotIndex > 0) { const ns = resolveSetupReference(tag.slice(0, dotIndex), context) @@ -435,3 +441,9 @@ function mergePropValues(existing: IRProp, incoming: IRProp) { function isComponentTag(tag: string) { return tag === 'component' || tag === 'Component' } + +export function isBuiltInComponent(tag: string): string | undefined { + if (tag === 'Transition' || tag === 'transition') { + return 'Transition' + } +} From 11bcb21204f05aa3521f65d85190393a6e7f50db Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 4 Mar 2025 17:55:13 +0800 Subject: [PATCH 10/62] wip: handle keyed element transition --- .../compiler-vapor/src/generators/block.ts | 8 +++++-- .../src/generators/component.ts | 23 ++++++++++++++++--- packages/compiler-vapor/src/ir/index.ts | 1 + packages/compiler-vapor/src/transforms/vIf.ts | 15 ++++++++---- .../compiler-vapor/src/transforms/vSlot.ts | 19 +++++++++++++-- .../runtime-vapor/src/apiCreateFragment.ts | 10 ++++++++ packages/runtime-vapor/src/index.ts | 1 + 7 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 packages/runtime-vapor/src/apiCreateFragment.ts diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts index 4e568a91e7f..39354e252a9 100644 --- a/packages/compiler-vapor/src/generators/block.ts +++ b/packages/compiler-vapor/src/generators/block.ts @@ -13,6 +13,7 @@ import type { CodegenContext } from '../generate' import { genEffects, genOperations } from './operation' import { genChildren } from './template' import { toValidAssetId } from '@vue/compiler-dom' +import { genExpression } from './expression' export function genBlock( oper: BlockIRNode, @@ -40,7 +41,7 @@ export function genBlockContent( customReturns?: (returns: CodeFragment[]) => CodeFragment[], ): CodeFragment[] { const [frag, push] = buildCodeFragment() - const { dynamic, effect, operation, returns } = block + const { dynamic, effect, operation, returns, key } = block const resetBlock = context.enterBlock(block) if (root) { @@ -57,7 +58,10 @@ export function genBlockContent( if (dynamic.needsKey) { for (const child of dynamic.children) { - push(NEWLINE, `n${child.id}.key = ${JSON.stringify(child.id)}`) + const keyValue = key + ? genExpression(key, context) + : JSON.stringify(child.id) + push(NEWLINE, `n${child.id}.key = `, ...keyValue) } } diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 8b0ab73e78f..c0ac494641d 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -99,7 +99,7 @@ export function genCreateComponent( return `_${builtInTag}` } return genExpression( - extend(createSimpleExpression(operation.tag, false), { ast: null }), + extend(createSimpleExpression(tag, false), { ast: null }), context, ) } @@ -402,7 +402,7 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) { let propsName: string | undefined let exitScope: (() => void) | undefined let depth: number | undefined - const { props } = oper + const { props, key } = oper const idsOfProps = new Set() if (props) { @@ -430,11 +430,28 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) { ? `${propsName}[${JSON.stringify(id)}]` : null), ) - const blockFn = context.withId( + let blockFn = context.withId( () => genBlock(oper, context, [propsName]), idMap, ) exitScope && exitScope() + if (key) { + blockFn = [ + `() => {`, + INDENT_START, + NEWLINE, + `return `, + ...genCall( + context.helper('createKeyedFragment'), + [`() => `, ...genExpression(key, context)], + blockFn, + ), + INDENT_END, + NEWLINE, + `}`, + ] + } + return blockFn } diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index b048e40d79b..71e896e13f1 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -40,6 +40,7 @@ export enum IRNodeTypes { export interface BaseIRNode { type: IRNodeTypes + key?: SimpleExpressionNode | undefined } export type CoreHelper = keyof typeof import('packages/runtime-dom/src') diff --git a/packages/compiler-vapor/src/transforms/vIf.ts b/packages/compiler-vapor/src/transforms/vIf.ts index bf9b178fd34..5306cf70573 100644 --- a/packages/compiler-vapor/src/transforms/vIf.ts +++ b/packages/compiler-vapor/src/transforms/vIf.ts @@ -130,11 +130,16 @@ export function createIfBranch( return [branch, exitBlock] } -function isInTransition(context: TransformContext): boolean { +export function isInTransition( + context: TransformContext, +): boolean { const parentNode = context.parent && context.parent.node - return !!( - parentNode && - parentNode.type === NodeTypes.ELEMENT && - (parentNode.tag === 'transition' || parentNode.tag === 'Transition') + return !!(parentNode && isTransitionNode(parentNode as ElementNode)) +} + +export function isTransitionNode(node: ElementNode): boolean { + return ( + node.type === NodeTypes.ELEMENT && + (node.tag === 'transition' || node.tag === 'Transition') ) } diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index d1bf1c6b05f..c1b82e2bc57 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -23,7 +23,8 @@ import { type SlotBlockIRNode, type VaporDirectiveNode, } from '../ir' -import { findDir, resolveExpression } from '../utils' +import { findDir, findProp, resolveExpression } from '../utils' +import { isTransitionNode } from './vIf' export const transformVSlot: NodeTransform = (node, context) => { if (node.type !== NodeTypes.ELEMENT) return @@ -72,7 +73,18 @@ function transformComponentSlot( !(n.type === NodeTypes.ELEMENT && n.props.some(isVSlot)), ) - const [block, onExit] = createSlotBlock(node, dir, context) + let slotKey + if (isTransitionNode(node)) { + const keyProp = findProp( + nonSlotTemplateChildren[0] as ElementNode, + 'key', + ) as VaporDirectiveNode + if (keyProp) { + slotKey = keyProp.exp + } + } + + const [block, onExit] = createSlotBlock(node, dir, context, slotKey) const { slots } = context @@ -233,9 +245,12 @@ function createSlotBlock( slotNode: ElementNode, dir: VaporDirectiveNode | undefined, context: TransformContext, + key: SimpleExpressionNode | undefined = undefined, ): [SlotBlockIRNode, () => void] { const block: SlotBlockIRNode = newBlock(slotNode) block.props = dir && dir.exp + block.key = key + if (key) block.dynamic.needsKey = true const exitBlock = context.enterBlock(block) return [block, exitBlock] } diff --git a/packages/runtime-vapor/src/apiCreateFragment.ts b/packages/runtime-vapor/src/apiCreateFragment.ts new file mode 100644 index 00000000000..50179b89ef9 --- /dev/null +++ b/packages/runtime-vapor/src/apiCreateFragment.ts @@ -0,0 +1,10 @@ +import { type Block, type BlockFn, DynamicFragment } from './block' +import { renderEffect } from './renderEffect' + +export function createKeyedFragment(key: () => any, render: BlockFn): Block { + const frag = __DEV__ ? new DynamicFragment('keyed') : new DynamicFragment() + renderEffect(() => { + frag.update(render, key()) + }) + return frag +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 40a847ba8f5..c356727f862 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -24,6 +24,7 @@ export { } from './dom/prop' export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event' export { createIf } from './apiCreateIf' +export { createKeyedFragment } from './apiCreateFragment' export { createFor, createForSlots, From 90dc4e206436b4b074a39cc364f800909c188966 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Mar 2025 08:27:57 +0800 Subject: [PATCH 11/62] wip: refactor --- packages/runtime-vapor/src/block.ts | 24 +++++++------------ .../src/components/Transition.ts | 19 +++++++++++---- .../runtime-vapor/src/directives/vShow.ts | 10 ++++---- packages/runtime-vapor/src/dom/prop.ts | 1 + 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 96c4d94d423..6dbc05d2195 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -35,7 +35,7 @@ export interface VaporTransitionHooks extends TransitionHooks { export type TransitionBlock = { key?: any - transition?: VaporTransitionHooks + $transition?: VaporTransitionHooks } export type BlockFn = (...args: any[]) => Block @@ -45,7 +45,7 @@ export class VaporFragment { anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void - transitionChild?: TransitionBlock | undefined + $transition?: VaporTransitionHooks | undefined constructor(nodes: Block) { this.nodes = nodes @@ -57,7 +57,6 @@ export class DynamicFragment extends VaporFragment { scope: EffectScope | undefined current?: BlockFn fallback?: BlockFn - transitionChild?: Block constructor(anchorLabel?: string) { super([]) @@ -74,16 +73,13 @@ export class DynamicFragment extends VaporFragment { pauseTracking() const parent = this.anchor.parentNode - const transition = this.transition + const transition = this.$transition const renderBranch = () => { if (render) { this.scope = new EffectScope() this.nodes = this.scope.run(render) || [] if (transition) { - this.transitionChild = applyTransitionEnterHooks( - this.nodes, - transition, - ) + this.$transition = applyTransitionEnterHooks(this.nodes, transition) } if (parent) insert(this.nodes, parent, this.anchor) } else { @@ -120,10 +116,6 @@ export class DynamicFragment extends VaporFragment { resetTracking() } - - get transition(): VaporTransitionHooks | undefined { - return this.transitionChild && this.transitionChild.transition - } } export function isFragment(val: NonNullable): val is VaporFragment { @@ -161,11 +153,11 @@ export function insert( anchor = anchor === 0 ? parent.firstChild : anchor if (block instanceof Node) { // don't apply transition on text or comment nodes - if (block.transition && block instanceof Element) { + if (block.$transition && block instanceof Element) { performTransitionEnter( block, // @ts-expect-error - block.transition, + block.$transition, () => parent.insertBefore(block, anchor), parentSuspense, ) @@ -196,11 +188,11 @@ export function prepend(parent: ParentNode, ...blocks: Block[]): void { export function remove(block: Block, parent?: ParentNode): void { if (block instanceof Node) { - if (block.transition && block instanceof Element) { + if (block.$transition && block instanceof Element) { performTransitionLeave( block, // @ts-expect-error - block.transition, + block.$transition, () => parent && parent.removeChild(block), ) } else { diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 210c0855ef6..8a705fda029 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -111,18 +111,19 @@ function resolveTransitionHooks( function setTransitionHooks(block: Block, hooks: VaporTransitionHooks) { if (!isFragment(block)) { - block.transition = hooks + block.$transition = hooks } } export function applyTransitionEnterHooks( block: Block, hooks: VaporTransitionHooks, -): Block | undefined { +): VaporTransitionHooks | undefined { const child = findElementChild(block) + let enterHooks if (child) { const { props, state, delayedLeave } = hooks - let enterHooks = resolveTransitionHooks( + enterHooks = resolveTransitionHooks( child, props, state, @@ -132,10 +133,10 @@ export function applyTransitionEnterHooks( enterHooks.delayedLeave = delayedLeave setTransitionHooks(child, enterHooks) if (isFragment(block)) { - block.transitionChild = child + block.$transition = enterHooks } } - return child + return enterHooks } export function applyTransitionLeaveHooks( @@ -161,6 +162,7 @@ export function applyTransitionLeaveHooks( leavingHooks.afterLeave = () => { state.isLeaving = false afterLeaveCb() + leavingBlock.$transition = undefined delete leavingHooks.afterLeave } } else if (mode === 'in-out') { @@ -174,17 +176,24 @@ export function applyTransitionLeaveHooks( block[leaveCbKey] = () => { earlyRemove() block[leaveCbKey] = undefined + leavingBlock.$transition = undefined delete enterHooks.delayedLeave } enterHooks.delayedLeave = () => { delayedLeave() + leavingBlock.$transition = undefined delete enterHooks.delayedLeave } } } } +const transitionChildCache = new WeakMap() export function findElementChild(block: Block): Block | undefined { + if (transitionChildCache.has(block)) { + return transitionChildCache.get(block) + } + let child: Block | undefined if (block instanceof Node) { // transition can only be applied on Element child diff --git a/packages/runtime-vapor/src/directives/vShow.ts b/packages/runtime-vapor/src/directives/vShow.ts index 492d5225ef2..82855fef847 100644 --- a/packages/runtime-vapor/src/directives/vShow.ts +++ b/packages/runtime-vapor/src/directives/vShow.ts @@ -39,20 +39,20 @@ function setDisplay(target: Block, value: unknown): void { if (target instanceof DynamicFragment) { return setDisplay(target.nodes, value) } - const { transition } = target + 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 } - if (transition) { + if ($transition) { if (value) { - transition.beforeEnter(target) + $transition.beforeEnter(target) el.style.display = el[vShowOriginalDisplay]! - transition.enter(target) + $transition.enter(target) } else { - transition.leave(target, () => { + $transition.leave(target, () => { el.style.display = 'none' }) } diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts index f464a2f6299..377d67c9349 100644 --- a/packages/runtime-vapor/src/dom/prop.ts +++ b/packages/runtime-vapor/src/dom/prop.ts @@ -267,6 +267,7 @@ export function optimizePropertyLookup(): void { if (isOptimized) return isOptimized = true const proto = Element.prototype as any + proto.$transition = undefined proto.$evtclick = undefined proto.$root = false proto.$html = From 3fcba1d5aa92cf309b02015f8e06f187fe87fd54 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Mar 2025 08:59:04 +0800 Subject: [PATCH 12/62] wip: improve types --- packages/runtime-vapor/src/block.ts | 38 +++++++++---------- .../src/components/Transition.ts | 25 +++++++----- .../runtime-vapor/src/directives/vShow.ts | 4 +- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 6dbc05d2195..73b3af38f43 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -19,33 +19,32 @@ import { applyTransitionLeaveHooks, } from './components/Transition' -export type Block = ( - | Node - | VaporFragment - | DynamicFragment - | VaporComponentInstance - | Block[] -) & - TransitionBlock +export interface TransitionOptions { + key?: any + $transition?: VaporTransitionHooks +} export interface VaporTransitionHooks extends TransitionHooks { state: TransitionState props: TransitionProps } -export type TransitionBlock = { - key?: any - $transition?: VaporTransitionHooks -} +export type TransitionBlock = + | (Node & TransitionOptions) + | (VaporFragment & TransitionOptions) + | (DynamicFragment & TransitionOptions) + +export type Block = TransitionBlock | VaporComponentInstance | Block[] export type BlockFn = (...args: any[]) => Block -export class VaporFragment { +export class VaporFragment implements TransitionOptions { + key?: any + $transition?: VaporTransitionHooks | undefined nodes: Block anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void - $transition?: VaporTransitionHooks | undefined constructor(nodes: Block) { this.nodes = nodes @@ -72,7 +71,6 @@ export class DynamicFragment extends VaporFragment { pauseTracking() const parent = this.anchor.parentNode - const transition = this.$transition const renderBranch = () => { if (render) { @@ -153,11 +151,10 @@ export function insert( anchor = anchor === 0 ? parent.firstChild : anchor if (block instanceof Node) { // don't apply transition on text or comment nodes - if (block.$transition && block instanceof Element) { + if ((block as TransitionBlock).$transition && block instanceof Element) { performTransitionEnter( block, - // @ts-expect-error - block.$transition, + (block as TransitionBlock).$transition as TransitionHooks, () => parent.insertBefore(block, anchor), parentSuspense, ) @@ -188,11 +185,10 @@ export function prepend(parent: ParentNode, ...blocks: Block[]): void { export function remove(block: Block, parent?: ParentNode): void { if (block instanceof Node) { - if (block.$transition && block instanceof Element) { + if ((block as TransitionBlock).$transition && block instanceof Element) { performTransitionLeave( block, - // @ts-expect-error - block.$transition, + (block as TransitionBlock).$transition as TransitionHooks, () => parent && parent.removeChild(block), ) } else { diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 8a705fda029..44f87023edc 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -13,7 +13,12 @@ import { useTransitionState, warn, } from '@vue/runtime-dom' -import { type Block, type VaporTransitionHooks, isFragment } from '../block' +import { + type Block, + type TransitionBlock, + type VaporTransitionHooks, + isFragment, +} from '../block' import { isVaporComponent } from '../component' export const vaporTransitionImpl: VaporTransitionInterface = { @@ -85,7 +90,7 @@ const getTransitionHooksContext = ( } function resolveTransitionHooks( - block: Block, + block: TransitionBlock, props: TransitionProps, state: TransitionState, instance: GenericComponentInstance, @@ -109,7 +114,10 @@ function resolveTransitionHooks( return hooks } -function setTransitionHooks(block: Block, hooks: VaporTransitionHooks) { +function setTransitionHooks( + block: TransitionBlock, + hooks: VaporTransitionHooks, +) { if (!isFragment(block)) { block.$transition = hooks } @@ -188,20 +196,20 @@ export function applyTransitionLeaveHooks( } } -const transitionChildCache = new WeakMap() -export function findElementChild(block: Block): Block | undefined { +const transitionChildCache = new WeakMap() +export function findElementChild(block: Block): TransitionBlock | undefined { if (transitionChildCache.has(block)) { return transitionChildCache.get(block) } - let child: Block | undefined + let child: TransitionBlock | undefined if (block instanceof Node) { // transition can only be applied on Element child if (block instanceof Element) child = block } else if (isVaporComponent(block)) { child = findElementChild(block.block) } else if (Array.isArray(block)) { - child = block[0] + child = block[0] as TransitionBlock let hasFound = false for (const c of block) { const item = findElementChild(c) @@ -219,8 +227,7 @@ export function findElementChild(block: Block): Block | undefined { if (!__DEV__) break } } - } else { - // fragment + } else if (isFragment(block)) { child = findElementChild(block.nodes) } diff --git a/packages/runtime-vapor/src/directives/vShow.ts b/packages/runtime-vapor/src/directives/vShow.ts index 82855fef847..410f0da235f 100644 --- a/packages/runtime-vapor/src/directives/vShow.ts +++ b/packages/runtime-vapor/src/directives/vShow.ts @@ -6,7 +6,7 @@ import { } from '@vue/runtime-dom' import { renderEffect } from '../renderEffect' import { isVaporComponent } from '../component' -import { type Block, DynamicFragment } from '../block' +import { type Block, DynamicFragment, type TransitionBlock } from '../block' import { isArray } from '@vue/shared' export function applyVShow(target: Block, source: () => any): void { @@ -39,7 +39,7 @@ function setDisplay(target: Block, value: unknown): void { if (target instanceof DynamicFragment) { return setDisplay(target.nodes, value) } - const { $transition } = target + const { $transition } = target as TransitionBlock if (target instanceof Element) { const el = target as VShowElement if (!(vShowOriginalDisplay in el)) { From 5dce316d193109a315834784b4ded3c5e8a19b1d Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Mar 2025 10:12:19 +0800 Subject: [PATCH 13/62] wip: inject useVaporTransition call for treeshaking --- packages/compiler-vapor/src/generate.ts | 5 ++ packages/compiler-vapor/src/ir/index.ts | 1 + packages/compiler-vapor/src/transform.ts | 1 + packages/compiler-vapor/src/transforms/vIf.ts | 16 +++-- .../compiler-vapor/src/transforms/vSlot.ts | 2 +- packages/runtime-vapor/src/apiCreateApp.ts | 2 - .../src/components/Transition.ts | 58 +++++++++---------- packages/runtime-vapor/src/index.ts | 1 + packages/runtime-vapor/src/vdomInterop.ts | 2 - 9 files changed, 50 insertions(+), 38 deletions(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 193a0f5da77..64d346d1d59 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -129,6 +129,11 @@ export function generate( `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`, ) } + + if (ir.hasTransition) { + push(NEWLINE, `${context.helper('useVaporTransition')}()`) + } + push(...genBlockContent(ir.block, context, true)) push(INDENT_END, NEWLINE) diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 71e896e13f1..6c80662a57d 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -68,6 +68,7 @@ export interface RootIRNode { directive: Set block: BlockIRNode hasTemplateRef: boolean + hasTransition: boolean } export interface IfIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 76563899d2b..788b1889ab9 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -230,6 +230,7 @@ export function transform( directive: new Set(), block: newBlock(node), hasTemplateRef: false, + hasTransition: false, } const context = new TransformContext(ir, node, options) diff --git a/packages/compiler-vapor/src/transforms/vIf.ts b/packages/compiler-vapor/src/transforms/vIf.ts index 5306cf70573..91513434957 100644 --- a/packages/compiler-vapor/src/transforms/vIf.ts +++ b/packages/compiler-vapor/src/transforms/vIf.ts @@ -134,12 +134,20 @@ export function isInTransition( context: TransformContext, ): boolean { const parentNode = context.parent && context.parent.node - return !!(parentNode && isTransitionNode(parentNode as ElementNode)) + return !!(parentNode && isTransitionNode(parentNode as ElementNode, context)) } -export function isTransitionNode(node: ElementNode): boolean { - return ( +export function isTransitionNode( + node: ElementNode, + context: TransformContext, +): boolean { + const inTransition = node.type === NodeTypes.ELEMENT && (node.tag === 'transition' || node.tag === 'Transition') - ) + + if (inTransition) { + context.ir.hasTransition = true + } + + return inTransition } diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index c1b82e2bc57..d14f91b58c4 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -74,7 +74,7 @@ function transformComponentSlot( ) let slotKey - if (isTransitionNode(node)) { + if (isTransitionNode(node, context)) { const keyProp = findProp( nonSlotTemplateChildren[0] as ElementNode, 'key', diff --git a/packages/runtime-vapor/src/apiCreateApp.ts b/packages/runtime-vapor/src/apiCreateApp.ts index da09a79a12a..8088e1aee6d 100644 --- a/packages/runtime-vapor/src/apiCreateApp.ts +++ b/packages/runtime-vapor/src/apiCreateApp.ts @@ -20,13 +20,11 @@ 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/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 44f87023edc..a7f540fa6ae 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -21,6 +21,7 @@ import { } from '../block' import { isVaporComponent } from '../component' +/*#__NO_SIDE_EFFECTS__*/ export const vaporTransitionImpl: VaporTransitionInterface = { applyTransition: ( props: TransitionProps, @@ -118,31 +119,29 @@ function setTransitionHooks( block: TransitionBlock, hooks: VaporTransitionHooks, ) { - if (!isFragment(block)) { - block.$transition = hooks - } + block.$transition = hooks } export function applyTransitionEnterHooks( block: Block, hooks: VaporTransitionHooks, -): VaporTransitionHooks | undefined { - const child = findElementChild(block) - let enterHooks - if (child) { - const { props, state, delayedLeave } = hooks - enterHooks = resolveTransitionHooks( - child, - props, - state, - currentInstance!, - hooks => (enterHooks = hooks as VaporTransitionHooks), - ) - enterHooks.delayedLeave = delayedLeave - setTransitionHooks(child, enterHooks) - if (isFragment(block)) { - block.$transition = enterHooks - } +): VaporTransitionHooks { + const child = findTransitionBlock(block) + if (!child) return hooks + + const { props, state, delayedLeave } = hooks + let enterHooks = resolveTransitionHooks( + child, + props, + state, + currentInstance!, + hooks => (enterHooks = hooks as VaporTransitionHooks), + ) + enterHooks.delayedLeave = delayedLeave + setTransitionHooks(child, enterHooks) + if (isFragment(block)) { + // also set transition hooks on fragment for reusing during it's updating + setTransitionHooks(block, enterHooks) } return enterHooks } @@ -152,7 +151,7 @@ export function applyTransitionLeaveHooks( enterHooks: VaporTransitionHooks, afterLeaveCb: () => void, ): void { - const leavingBlock = findElementChild(block) + const leavingBlock = findTransitionBlock(block) if (!leavingBlock) return undefined const { props, state } = enterHooks @@ -196,10 +195,10 @@ export function applyTransitionLeaveHooks( } } -const transitionChildCache = new WeakMap() -export function findElementChild(block: Block): TransitionBlock | undefined { - if (transitionChildCache.has(block)) { - return transitionChildCache.get(block) +const transitionBlockCache = new WeakMap() +export function findTransitionBlock(block: Block): TransitionBlock | undefined { + if (transitionBlockCache.has(block)) { + return transitionBlockCache.get(block) } let child: TransitionBlock | undefined @@ -207,12 +206,12 @@ export function findElementChild(block: Block): TransitionBlock | undefined { // transition can only be applied on Element child if (block instanceof Element) child = block } else if (isVaporComponent(block)) { - child = findElementChild(block.block) + child = findTransitionBlock(block.block) } else if (Array.isArray(block)) { child = block[0] as TransitionBlock let hasFound = false for (const c of block) { - const item = findElementChild(c) + const item = findTransitionBlock(c) if (item instanceof Element) { if (__DEV__ && hasFound) { // warn more than one non-comment child @@ -228,7 +227,7 @@ export function findElementChild(block: Block): TransitionBlock | undefined { } } } else if (isFragment(block)) { - child = findElementChild(block.nodes) + child = findTransitionBlock(block.nodes) } if (__DEV__ && !child) { @@ -239,7 +238,8 @@ export function findElementChild(block: Block): TransitionBlock | undefined { } let registered = false -export function ensureVaporTransition(): void { +/*#__NO_SIDE_EFFECTS__*/ +export function useVaporTransition(): void { if (!registered) { registerVaporTransition(vaporTransitionImpl) registered = true diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index c356727f862..4a9986b43ab 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -42,3 +42,4 @@ export { applyDynamicModel, } from './directives/vModel' export { withVaporDirectives } from './directives/custom' +export { useVaporTransition } from './components/Transition' diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index efe8223c604..77228fd72a0 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -33,7 +33,6 @@ 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< @@ -289,7 +288,6 @@ export const vaporInteropPlugin: Plugin = app => { const mount = app.mount app.mount = ((...args) => { optimizePropertyLookup() - ensureVaporTransition() return mount(...args) }) satisfies App['mount'] } From b92ea0a38a31ab1d241899721d4d3142bf77db80 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Mar 2025 11:43:24 +0800 Subject: [PATCH 14/62] wip: save --- .../runtime-dom/src/components/Transition.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/runtime-dom/src/components/Transition.ts b/packages/runtime-dom/src/components/Transition.ts index 90cdaba4e73..4b33566e379 100644 --- a/packages/runtime-dom/src/components/Transition.ts +++ b/packages/runtime-dom/src/components/Transition.ts @@ -7,6 +7,7 @@ import { assertNumber, compatUtils, h, + isVNode, } from '@vue/runtime-core' import { extend, isArray, isObject, toNumber } from '@vue/shared' @@ -99,12 +100,20 @@ const decorate = (t: typeof Transition) => { * base Transition component, with DOM-specific logic. */ export const Transition: FunctionalComponent = - /*@__PURE__*/ decorate((props, { slots, vapor }: any) => { + /*@__PURE__*/ decorate((props, { slots }) => { + const children = slots.default && slots.default() + const isVNodeChildren = isArray(children) && children.some(c => isVNode(c)) const resolvedProps = resolveTransitionProps(props) - if (vapor) { - return vaporTransitionImpl!.applyTransition(resolvedProps, slots) + if (isVNodeChildren) { + return h(BaseTransition, resolvedProps, { + default: () => children, + }) } - return h(BaseTransition, resolvedProps, slots) + + // vapor transition + return vaporTransitionImpl!.applyTransition(resolvedProps, { + default: () => children, + }) }) /** From e3f8ba4bf5451d8820af091eef04f50ff2179ab2 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Mar 2025 16:06:00 +0800 Subject: [PATCH 15/62] wip: save --- .../runtime-dom/src/components/Transition.ts | 17 +++++------------ packages/runtime-vapor/src/component.ts | 3 ++- packages/runtime-vapor/src/componentSlots.ts | 18 +++++++++++++++++- .../runtime-vapor/src/components/Transition.ts | 2 +- packages/runtime-vapor/src/vdomInterop.ts | 16 +++++----------- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/runtime-dom/src/components/Transition.ts b/packages/runtime-dom/src/components/Transition.ts index 4b33566e379..3d8ac9dc8b3 100644 --- a/packages/runtime-dom/src/components/Transition.ts +++ b/packages/runtime-dom/src/components/Transition.ts @@ -7,7 +7,6 @@ import { assertNumber, compatUtils, h, - isVNode, } from '@vue/runtime-core' import { extend, isArray, isObject, toNumber } from '@vue/shared' @@ -37,7 +36,7 @@ export interface VaporTransitionInterface { applyTransition: ( props: TransitionProps, slots: { default: () => any }, - ) => void + ) => any } let vaporTransitionImpl: VaporTransitionInterface | null = null @@ -101,19 +100,13 @@ const decorate = (t: typeof Transition) => { */ export const Transition: FunctionalComponent = /*@__PURE__*/ decorate((props, { slots }) => { - const children = slots.default && slots.default() - const isVNodeChildren = isArray(children) && children.some(c => isVNode(c)) const resolvedProps = resolveTransitionProps(props) - if (isVNodeChildren) { - return h(BaseTransition, resolvedProps, { - default: () => children, - }) + if (slots._vapor) { + // vapor transition + return vaporTransitionImpl!.applyTransition(resolvedProps, slots as any) } - // vapor transition - return vaporTransitionImpl!.applyTransition(resolvedProps, { - default: () => children, - }) + return h(BaseTransition, resolvedProps, slots) }) /** diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 3c39612bb89..2eae68e5fe5 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -56,6 +56,7 @@ import { type VaporSlot, dynamicSlotsProxyHandlers, getSlot, + vaporSlotsProxyHandler, } from './componentSlots' import { hmrReload, hmrRerender } from './hmr' @@ -416,7 +417,7 @@ export class VaporComponentInstance implements GenericComponentInstance { this.slots = rawSlots ? rawSlots.$ ? new Proxy(rawSlots, dynamicSlotsProxyHandlers) - : rawSlots + : new Proxy(rawSlots, vaporSlotsProxyHandler) : EMPTY_OBJ } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 9f6c2ba5a0d..0dd1329e44a 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -16,8 +16,24 @@ export type DynamicSlot = { name: string; fn: VaporSlot } export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[] export type DynamicSlotSource = StaticSlots | DynamicSlotFn +export const vaporSlotsProxyHandler: ProxyHandler = { + get(target, key) { + if (key === '_vapor') { + return target + } else { + return target[key] + } + }, +} + export const dynamicSlotsProxyHandlers: ProxyHandler = { - get: getSlot, + get: (target, key: string) => { + if (key === '_vapor') { + return target + } else { + return getSlot(target, key) + } + }, has: (target, key: string) => !!getSlot(target, key), getOwnPropertyDescriptor(target, key: string) { const slot = getSlot(target, key) diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index a7f540fa6ae..f6491aee356 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -26,7 +26,7 @@ export const vaporTransitionImpl: VaporTransitionInterface = { applyTransition: ( props: TransitionProps, slots: { default: () => Block }, - ) => { + ): Block | undefined => { const children = slots.default && slots.default() if (!children) return diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 77228fd72a0..b4ed4cb4d52 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -29,7 +29,11 @@ import { import { type Block, VaporFragment, insert, remove } from './block' import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' -import type { RawSlots, VaporSlot } from './componentSlots' +import { + type RawSlots, + type VaporSlot, + vaporSlotsProxyHandler, +} from './componentSlots' import { renderEffect } from './renderEffect' import { createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' @@ -129,16 +133,6 @@ const vaporSlotPropsProxyHandler: ProxyHandler< }, } -const vaporSlotsProxyHandler: ProxyHandler = { - get(target, key) { - if (key === '_vapor') { - return target - } else { - return target[key] - } - }, -} - /** * Mount vdom component in vapor */ From 7c68b482c58e76b58ecfd9fbf2abc3a306943073 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Mar 2025 17:09:20 +0800 Subject: [PATCH 16/62] wip: vdom interop --- .../runtime-core/src/helpers/renderSlot.ts | 2 +- .../runtime-dom/src/components/Transition.ts | 18 ++++++++++++++++-- packages/runtime-vapor/src/component.ts | 4 ++-- packages/runtime-vapor/src/componentSlots.ts | 10 +++++----- packages/runtime-vapor/src/vdomInterop.ts | 19 ++++++++++++++----- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index 152c5a4b81c..e41a14c2ae3 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -35,7 +35,7 @@ export function renderSlot( let slot = slots[name] // vapor slots rendered in vdom - if (slot && slots._vapor) { + if (slot && slots.__interop) { const ret = (openBlock(), createBlock(VaporSlot, props)) ret.vs = { slot, fallback } return ret diff --git a/packages/runtime-dom/src/components/Transition.ts b/packages/runtime-dom/src/components/Transition.ts index 3d8ac9dc8b3..7e28dc08301 100644 --- a/packages/runtime-dom/src/components/Transition.ts +++ b/packages/runtime-dom/src/components/Transition.ts @@ -4,9 +4,11 @@ import { BaseTransitionPropsValidators, DeprecationTypes, type FunctionalComponent, + type Slots, assertNumber, compatUtils, h, + renderSlot, } from '@vue/runtime-core' import { extend, isArray, isObject, toNumber } from '@vue/shared' @@ -101,8 +103,20 @@ const decorate = (t: typeof Transition) => { export const Transition: FunctionalComponent = /*@__PURE__*/ decorate((props, { slots }) => { const resolvedProps = resolveTransitionProps(props) - if (slots._vapor) { - // vapor transition + if (slots.__vapor) { + // with vapor interop plugin + if (slots.__interop) { + const children = vaporTransitionImpl!.applyTransition( + resolvedProps, + slots as any, + ) + const vaporSlots = { + default: () => children, + __interop: true, + } as any as Slots + return renderSlot(vaporSlots, 'default') + } + return vaporTransitionImpl!.applyTransition(resolvedProps, slots as any) } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 2eae68e5fe5..bd033d66538 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -56,7 +56,7 @@ import { type VaporSlot, dynamicSlotsProxyHandlers, getSlot, - vaporSlotsProxyHandler, + staticSlotsProxyHandler, } from './componentSlots' import { hmrReload, hmrRerender } from './hmr' @@ -417,7 +417,7 @@ export class VaporComponentInstance implements GenericComponentInstance { this.slots = rawSlots ? rawSlots.$ ? new Proxy(rawSlots, dynamicSlotsProxyHandlers) - : new Proxy(rawSlots, vaporSlotsProxyHandler) + : new Proxy(rawSlots, staticSlotsProxyHandler) : EMPTY_OBJ } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 0dd1329e44a..85bf52c0be5 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -16,10 +16,10 @@ export type DynamicSlot = { name: string; fn: VaporSlot } export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[] export type DynamicSlotSource = StaticSlots | DynamicSlotFn -export const vaporSlotsProxyHandler: ProxyHandler = { +export const staticSlotsProxyHandler: ProxyHandler = { get(target, key) { - if (key === '_vapor') { - return target + if (key === '__vapor') { + return true } else { return target[key] } @@ -28,8 +28,8 @@ export const vaporSlotsProxyHandler: ProxyHandler = { export const dynamicSlotsProxyHandlers: ProxyHandler = { get: (target, key: string) => { - if (key === '_vapor') { - return target + if (key === '__vapor') { + return true } else { return getSlot(target, key) } diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index b4ed4cb4d52..f6dbd267396 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -29,11 +29,7 @@ import { import { type Block, VaporFragment, insert, remove } from './block' import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' -import { - type RawSlots, - type VaporSlot, - vaporSlotsProxyHandler, -} from './componentSlots' +import type { RawSlots, VaporSlot } from './componentSlots' import { renderEffect } from './renderEffect' import { createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' @@ -133,6 +129,16 @@ const vaporSlotPropsProxyHandler: ProxyHandler< }, } +const vaporSlotsProxyHandler: ProxyHandler = { + get(target, key) { + if (key === '__interop') { + return target + } else { + return target[key] + } + }, +} + /** * Mount vdom component in vapor */ @@ -170,6 +176,8 @@ function createVDOMComponent( } frag.insert = (parentNode, anchor) => { + const prev = currentInstance + simpleSetCurrentInstance(parentInstance) if (!isMounted) { internals.mt( vnode, @@ -192,6 +200,7 @@ function createVDOMComponent( parentInstance as any, ) } + simpleSetCurrentInstance(prev) } frag.remove = unmount From 2e45f06ad353ba709b0669b41c19c8dbafa800ac Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Mar 2025 22:10:13 +0800 Subject: [PATCH 17/62] wip: refactor --- packages/compiler-vapor/src/generate.ts | 4 -- .../src/generators/component.ts | 3 +- packages/compiler-vapor/src/ir/index.ts | 1 - packages/compiler-vapor/src/transform.ts | 1 - .../src/transforms/transformElement.ts | 10 +---- packages/compiler-vapor/src/transforms/vIf.ts | 25 +----------- .../compiler-vapor/src/transforms/vSlot.ts | 10 +++-- packages/compiler-vapor/src/utils.ts | 23 +++++++++++ .../runtime-dom/src/components/Transition.ts | 40 ++----------------- packages/runtime-dom/src/index.ts | 5 --- .../src/components/Transition.ts | 35 +++++++--------- packages/runtime-vapor/src/index.ts | 2 +- 12 files changed, 53 insertions(+), 106 deletions(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 64d346d1d59..e6d6a816ce7 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -130,10 +130,6 @@ export function generate( ) } - if (ir.hasTransition) { - push(NEWLINE, `${context.helper('useVaporTransition')}()`) - } - push(...genBlockContent(ir.block, context, true)) push(INDENT_END, NEWLINE) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index c0ac494641d..d6ade4ebd91 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -39,7 +39,8 @@ import { genEventHandler } from './event' import { genDirectiveModifiers, genDirectivesForElement } from './directive' import { genBlock } from './block' import { genModelHandler } from './vModel' -import { isBuiltInComponent } from '../transforms/transformElement' + +import { isBuiltInComponent } from '../utils' export function genCreateComponent( operation: CreateComponentIRNode, diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 387bf113638..cc0c9dcf307 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -68,7 +68,6 @@ export interface RootIRNode { directive: Set block: BlockIRNode hasTemplateRef: boolean - hasTransition: boolean } export interface IfIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 788b1889ab9..76563899d2b 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -230,7 +230,6 @@ export function transform( directive: new Set(), block: newBlock(node), hasTemplateRef: false, - hasTransition: false, } const context = new TransformContext(ir, node, options) diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 14cdf66c2ee..35fd596ee83 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -1,4 +1,3 @@ -import { isValidHTMLNesting } from '@vue/compiler-dom' import { type AttributeNode, type ComponentNode, @@ -11,6 +10,7 @@ import { createCompilerError, createSimpleExpression, isStaticArgOf, + isValidHTMLNesting, } from '@vue/compiler-dom' import { camelize, @@ -36,7 +36,7 @@ import { type VaporDirectiveNode, } from '../ir' import { EMPTY_EXPRESSION } from './utils' -import { findProp } from '../utils' +import { findProp, isBuiltInComponent } from '../utils' export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap( // the leading comma is intentional so empty string "" is also included @@ -441,9 +441,3 @@ function mergePropValues(existing: IRProp, incoming: IRProp) { function isComponentTag(tag: string) { return tag === 'component' || tag === 'Component' } - -export function isBuiltInComponent(tag: string): string | undefined { - if (tag === 'Transition' || tag === 'transition') { - return 'Transition' - } -} diff --git a/packages/compiler-vapor/src/transforms/vIf.ts b/packages/compiler-vapor/src/transforms/vIf.ts index 91513434957..ad527a899a2 100644 --- a/packages/compiler-vapor/src/transforms/vIf.ts +++ b/packages/compiler-vapor/src/transforms/vIf.ts @@ -1,7 +1,6 @@ import { type ElementNode, ErrorCodes, - NodeTypes, createCompilerError, createSimpleExpression, } from '@vue/compiler-dom' @@ -19,7 +18,7 @@ import { import { extend } from '@vue/shared' import { newBlock, wrapTemplate } from './utils' import { getSiblingIf } from './transformComment' -import { isStaticExpression } from '../utils' +import { isInTransition, isStaticExpression } from '../utils' export const transformVIf: NodeTransform = createStructuralDirectiveTransform( ['if', 'else', 'else-if'], @@ -129,25 +128,3 @@ export function createIfBranch( branch.dynamic.needsKey = isInTransition(context) return [branch, exitBlock] } - -export function isInTransition( - context: TransformContext, -): boolean { - const parentNode = context.parent && context.parent.node - return !!(parentNode && isTransitionNode(parentNode as ElementNode, context)) -} - -export function isTransitionNode( - node: ElementNode, - context: TransformContext, -): boolean { - const inTransition = - node.type === NodeTypes.ELEMENT && - (node.tag === 'transition' || node.tag === 'Transition') - - if (inTransition) { - context.ir.hasTransition = true - } - - return inTransition -} diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index d14f91b58c4..66b24b0a9f0 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -23,8 +23,12 @@ import { type SlotBlockIRNode, type VaporDirectiveNode, } from '../ir' -import { findDir, findProp, resolveExpression } from '../utils' -import { isTransitionNode } from './vIf' +import { + findDir, + findProp, + isTransitionNode, + resolveExpression, +} from '../utils' export const transformVSlot: NodeTransform = (node, context) => { if (node.type !== NodeTypes.ELEMENT) return @@ -74,7 +78,7 @@ function transformComponentSlot( ) let slotKey - if (isTransitionNode(node, context)) { + if (isTransitionNode(node)) { const keyProp = findProp( nonSlotTemplateChildren[0] as ElementNode, 'key', diff --git a/packages/compiler-vapor/src/utils.ts b/packages/compiler-vapor/src/utils.ts index 728281914fd..d390c69a21a 100644 --- a/packages/compiler-vapor/src/utils.ts +++ b/packages/compiler-vapor/src/utils.ts @@ -15,6 +15,7 @@ import { } from '@vue/compiler-dom' import type { VaporDirectiveNode } from './ir' import { EMPTY_EXPRESSION } from './transforms/utils' +import type { TransformContext } from './transform' export const findProp = _findProp as ( node: ElementNode, @@ -88,3 +89,25 @@ export function getLiteralExpressionValue( } return exp.isStatic ? exp.content : null } + +export function isInTransition( + context: TransformContext, +): boolean { + const parentNode = context.parent && context.parent.node + return !!(parentNode && isTransitionNode(parentNode as ElementNode)) +} + +export function isTransitionNode(node: ElementNode): boolean { + return node.type === NodeTypes.ELEMENT && isTransitionTag(node.tag) +} + +export function isTransitionTag(tag: string): boolean { + tag = tag.toLowerCase() + return tag === 'transition' || tag === 'vaportransition' +} + +export function isBuiltInComponent(tag: string): string | undefined { + if (isTransitionTag(tag)) { + return 'VaporTransition' + } +} diff --git a/packages/runtime-dom/src/components/Transition.ts b/packages/runtime-dom/src/components/Transition.ts index 7e28dc08301..6c6344bfcac 100644 --- a/packages/runtime-dom/src/components/Transition.ts +++ b/packages/runtime-dom/src/components/Transition.ts @@ -4,11 +4,9 @@ import { BaseTransitionPropsValidators, DeprecationTypes, type FunctionalComponent, - type Slots, assertNumber, compatUtils, h, - renderSlot, } from '@vue/runtime-core' import { extend, isArray, isObject, toNumber } from '@vue/shared' @@ -34,20 +32,6 @@ export interface TransitionProps extends BaseTransitionProps { leaveToClass?: string } -export interface VaporTransitionInterface { - applyTransition: ( - props: TransitionProps, - slots: { default: () => any }, - ) => any -} - -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 { @@ -101,27 +85,9 @@ const decorate = (t: typeof Transition) => { * base Transition component, with DOM-specific logic. */ export const Transition: FunctionalComponent = - /*@__PURE__*/ decorate((props, { slots }) => { - const resolvedProps = resolveTransitionProps(props) - if (slots.__vapor) { - // with vapor interop plugin - if (slots.__interop) { - const children = vaporTransitionImpl!.applyTransition( - resolvedProps, - slots as any, - ) - const vaporSlots = { - default: () => children, - __interop: true, - } as any as Slots - return renderSlot(vaporSlots, 'default') - } - - return vaporTransitionImpl!.applyTransition(resolvedProps, slots as any) - } - - return h(BaseTransition, resolvedProps, slots) - }) + /*@__PURE__*/ decorate((props, { slots }) => + h(BaseTransition, resolveTransitionProps(props), 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 0cfd08e87a2..450ec74d15f 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -354,9 +354,4 @@ export { export { resolveTransitionProps, TransitionPropsValidators, - registerVaporTransition, } from './components/Transition' -/** - * @internal - */ -export type { VaporTransitionInterface } from './components/Transition' diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index f6491aee356..92952ad812a 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -1,15 +1,16 @@ import { + type FunctionalComponent, type GenericComponentInstance, type TransitionElement, type TransitionHooks, type TransitionHooksContext, type TransitionProps, + TransitionPropsValidators, type TransitionState, - type VaporTransitionInterface, baseResolveTransitionHooks, currentInstance, leaveCbKey, - registerVaporTransition, + resolveTransitionProps, useTransitionState, warn, } from '@vue/runtime-dom' @@ -21,13 +22,15 @@ import { } from '../block' import { isVaporComponent } from '../component' -/*#__NO_SIDE_EFFECTS__*/ -export const vaporTransitionImpl: VaporTransitionInterface = { - applyTransition: ( - props: TransitionProps, - slots: { default: () => Block }, - ): Block | undefined => { - const children = slots.default && slots.default() +const decorate = (t: typeof VaporTransition) => { + t.displayName = 'VaporTransition' + t.props = TransitionPropsValidators + return t +} + +export const VaporTransition: FunctionalComponent = + /*@__PURE__*/ decorate((props, { slots }) => { + const children = (slots.default && slots.default()) as any as Block if (!children) return const { mode } = props @@ -43,12 +46,11 @@ export const vaporTransitionImpl: VaporTransitionInterface = { applyTransitionEnterHooks(children, { state: useTransitionState(), - props, + props: resolveTransitionProps(props), } as VaporTransitionHooks) return children - }, -} + }) const getTransitionHooksContext = ( key: String, @@ -236,12 +238,3 @@ export function findTransitionBlock(block: Block): TransitionBlock | undefined { return child } - -let registered = false -/*#__NO_SIDE_EFFECTS__*/ -export function useVaporTransition(): void { - if (!registered) { - registerVaporTransition(vaporTransitionImpl) - registered = true - } -} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index ac34bef4d55..d663e179653 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -42,4 +42,4 @@ export { applyDynamicModel, } from './directives/vModel' export { withVaporDirectives } from './directives/custom' -export { useVaporTransition } from './components/Transition' +export { VaporTransition } from './components/Transition' From 6b9e9cee6ae8c49ab209c09a1dcd16950730ac96 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Mar 2025 22:21:46 +0800 Subject: [PATCH 18/62] wip: refactor --- packages/compiler-vapor/src/generate.ts | 1 - packages/runtime-vapor/src/components/Transition.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index e6d6a816ce7..193a0f5da77 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -129,7 +129,6 @@ export function generate( `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`, ) } - push(...genBlockContent(ir.block, context, true)) push(INDENT_END, NEWLINE) diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 92952ad812a..01326e5d98e 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -25,6 +25,7 @@ import { isVaporComponent } from '../component' const decorate = (t: typeof VaporTransition) => { t.displayName = 'VaporTransition' t.props = TransitionPropsValidators + t.__vapor = true return t } From d0faf6c992ac0a5d6ef70cbbf02593d883e06eff Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Mar 2025 22:35:14 +0800 Subject: [PATCH 19/62] wip: vdom interop --- .../runtime-vapor/src/apiCreateDynamicComponent.ts | 7 +++++-- packages/runtime-vapor/src/block.ts | 12 ++++++++---- packages/runtime-vapor/src/component.ts | 5 +++-- packages/runtime-vapor/src/components/Transition.ts | 6 +++++- packages/runtime-vapor/src/vdomInterop.ts | 8 ++++++-- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts index 2126611d718..c061c8224cd 100644 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@ -1,6 +1,6 @@ -import { resolveDynamicComponent } from '@vue/runtime-dom' +import { currentInstance, resolveDynamicComponent } from '@vue/runtime-dom' import { DynamicFragment, type VaporFragment } from './block' -import { createComponentWithFallback } from './component' +import { createComponentWithFallback, emptyContext } from './component' import { renderEffect } from './renderEffect' import type { RawProps } from './componentProps' import type { RawSlots } from './componentSlots' @@ -16,6 +16,8 @@ export function createDynamicComponent( : new DynamicFragment() renderEffect(() => { const value = getter() + const appContext = + (currentInstance && currentInstance.appContext) || emptyContext frag.update( () => createComponentWithFallback( @@ -23,6 +25,7 @@ export function createDynamicComponent( rawProps, rawSlots, isSingleRoot, + appContext, ), value, ) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 73b3af38f43..18313d191df 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -43,8 +43,12 @@ export class VaporFragment implements TransitionOptions { $transition?: VaporTransitionHooks | undefined nodes: Block anchor?: Node - insert?: (parent: ParentNode, anchor: Node | null) => void - remove?: (parent?: ParentNode) => void + insert?: ( + parent: ParentNode, + anchor: Node | null, + transitionHooks?: TransitionHooks, + ) => void + remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void constructor(nodes: Block) { this.nodes = nodes @@ -170,7 +174,7 @@ export function insert( } else { // fragment if (block.insert) { - block.insert(parent, anchor) + block.insert(parent, anchor, (block as TransitionBlock).$transition) } else { insert(block.nodes, parent, anchor, parentSuspense) } @@ -203,7 +207,7 @@ export function remove(block: Block, parent?: ParentNode): void { } else { // fragment if (block.remove) { - block.remove(parent) + block.remove(parent, (block as TransitionBlock).$transition) } else { remove(block.nodes, parent) } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index bd033d66538..b12ae3f2d47 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -278,7 +278,7 @@ export function devRender(instance: VaporComponentInstance): void { ) || [] } -const emptyContext: GenericAppContext = { +export const emptyContext: GenericAppContext = { app: null as any, config: {}, provides: /*@__PURE__*/ Object.create(null), @@ -446,9 +446,10 @@ export function createComponentWithFallback( rawProps?: LooseRawProps | null, rawSlots?: LooseRawSlots | null, isSingleRoot?: boolean, + appContext?: GenericAppContext, ): HTMLElement | VaporComponentInstance { if (!isString(comp)) { - return createComponent(comp, rawProps, rawSlots, isSingleRoot) + return createComponent(comp, rawProps, rawSlots, isSingleRoot, appContext) } const el = document.createElement(comp) diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 01326e5d98e..13ec46ad6dc 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -230,7 +230,11 @@ export function findTransitionBlock(block: Block): TransitionBlock | undefined { } } } else if (isFragment(block)) { - child = findTransitionBlock(block.nodes) + if (block.insert) { + child = block + } else { + child = findTransitionBlock(block.nodes) + } } if (__DEV__ && !child) { diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index f6dbd267396..dc3ff7c5e6d 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -7,6 +7,7 @@ import { type RendererInternals, type ShallowRef, type Slots, + type TransitionHooks, type VNode, type VaporInteropInterface, createVNode, @@ -14,6 +15,7 @@ import { ensureRenderer, onScopeDispose, renderSlot, + setTransitionHooks, shallowRef, simpleSetCurrentInstance, } from '@vue/runtime-dom' @@ -171,14 +173,16 @@ function createVDOMComponent( let isMounted = false const parentInstance = currentInstance as VaporComponentInstance - const unmount = (parentNode?: ParentNode) => { + const unmount = (parentNode?: ParentNode, transition?: TransitionHooks) => { + if (transition) setTransitionHooks(vnode, transition) internals.umt(vnode.component!, null, !!parentNode) } - frag.insert = (parentNode, anchor) => { + frag.insert = (parentNode, anchor, transition) => { const prev = currentInstance simpleSetCurrentInstance(parentInstance) if (!isMounted) { + if (transition) setTransitionHooks(vnode, transition) internals.mt( vnode, parentNode, From 31d9247eb7030a44d60e04551edc817195c1719e Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 6 Mar 2025 14:31:47 +0800 Subject: [PATCH 20/62] wip: vapor interop --- .../runtime-core/src/components/BaseTransition.ts | 12 +++++++++--- packages/runtime-core/src/renderer.ts | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 5f522b5cf6e..e2f9bdf26b5 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -139,7 +139,9 @@ export const BaseTransitionPropsValidators: Record = { } const recursiveGetSubtree = (instance: ComponentInternalInstance): VNode => { - const subTree = instance.subTree + const subTree = instance.type.__vapor + ? (instance as any).block + : instance.subTree return subTree.component ? recursiveGetSubtree(subTree.component) : subTree } @@ -564,8 +566,12 @@ function getInnerChild(vnode: VNode): VNode | undefined { export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks): void { if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) { - vnode.transition = hooks - setTransitionHooks(vnode.component.subTree, hooks) + if ((vnode.type as any).__vapor) { + ;(vnode.component as any).block.$transition = hooks + } else { + vnode.transition = hooks + setTransitionHooks(vnode.component.subTree, hooks) + } } else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) { vnode.ssContent!.transition = hooks.clone(vnode.ssContent!) vnode.ssFallback!.transition = hooks.clone(vnode.ssFallback!) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index a73b5557de2..39205ab1eac 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -2411,7 +2411,7 @@ function baseCreateRenderer( const getNextHostNode: NextFn = vnode => { if (vnode.shapeFlag & ShapeFlags.COMPONENT) { if ((vnode.type as ConcreteComponent).__vapor) { - return hostNextSibling((vnode.component! as any).block) + return hostNextSibling(vnode.anchor!) } return getNextHostNode(vnode.component!.subTree) } @@ -2608,7 +2608,7 @@ export function traverseStaticChildren( function locateNonHydratedAsyncRoot( instance: ComponentInternalInstance, ): ComponentInternalInstance | undefined { - const subComponent = instance.subTree.component + const subComponent = instance.subTree && instance.subTree.component if (subComponent) { if (subComponent.asyncDep && !subComponent.asyncResolved) { return subComponent From 3cb3e1ac3931248d55f21455522155ad317112a7 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 6 Mar 2025 14:35:16 +0800 Subject: [PATCH 21/62] wip: revert some changes --- .../runtime-core/src/helpers/renderSlot.ts | 2 +- packages/runtime-vapor/src/component.ts | 3 +-- packages/runtime-vapor/src/componentSlots.ts | 18 +----------------- packages/runtime-vapor/src/vdomInterop.ts | 2 +- 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index e41a14c2ae3..152c5a4b81c 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -35,7 +35,7 @@ export function renderSlot( let slot = slots[name] // vapor slots rendered in vdom - if (slot && slots.__interop) { + if (slot && slots._vapor) { const ret = (openBlock(), createBlock(VaporSlot, props)) ret.vs = { slot, fallback } return ret diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index b12ae3f2d47..25f909acc5e 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -56,7 +56,6 @@ import { type VaporSlot, dynamicSlotsProxyHandlers, getSlot, - staticSlotsProxyHandler, } from './componentSlots' import { hmrReload, hmrRerender } from './hmr' @@ -417,7 +416,7 @@ export class VaporComponentInstance implements GenericComponentInstance { this.slots = rawSlots ? rawSlots.$ ? new Proxy(rawSlots, dynamicSlotsProxyHandlers) - : new Proxy(rawSlots, staticSlotsProxyHandler) + : rawSlots : EMPTY_OBJ } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 85bf52c0be5..9f6c2ba5a0d 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -16,24 +16,8 @@ export type DynamicSlot = { name: string; fn: VaporSlot } export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[] export type DynamicSlotSource = StaticSlots | DynamicSlotFn -export const staticSlotsProxyHandler: ProxyHandler = { - get(target, key) { - if (key === '__vapor') { - return true - } else { - return target[key] - } - }, -} - export const dynamicSlotsProxyHandlers: ProxyHandler = { - get: (target, key: string) => { - if (key === '__vapor') { - return true - } else { - return getSlot(target, key) - } - }, + get: getSlot, has: (target, key: string) => !!getSlot(target, key), getOwnPropertyDescriptor(target, key: string) { const slot = getSlot(target, key) diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index dc3ff7c5e6d..44ec5105e9b 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -133,7 +133,7 @@ const vaporSlotPropsProxyHandler: ProxyHandler< const vaporSlotsProxyHandler: ProxyHandler = { get(target, key) { - if (key === '__interop') { + if (key === '_vapor') { return target } else { return target[key] From 41c258903efb5aa914a71ffdb5e9cda0efd998d5 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 6 Mar 2025 15:10:57 +0800 Subject: [PATCH 22/62] wip: add tests --- .../transforms/TransformTransition.spec.ts | 55 +++++++++++++++++++ .../TransformTransition.spec.ts.snap | 53 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 packages/compiler-vapor/__tests__/transforms/TransformTransition.spec.ts create mode 100644 packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap diff --git a/packages/compiler-vapor/__tests__/transforms/TransformTransition.spec.ts b/packages/compiler-vapor/__tests__/transforms/TransformTransition.spec.ts new file mode 100644 index 00000000000..28425e5fd38 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/TransformTransition.spec.ts @@ -0,0 +1,55 @@ +import { makeCompile } from './_utils' +import { + transformChildren, + transformElement, + transformText, + transformVBind, + transformVIf, + transformVSlot, +} from '@vue/compiler-vapor' +import { expect } from 'vitest' + +const compileWithElementTransform = makeCompile({ + nodeTransforms: [ + transformText, + transformVIf, + transformElement, + transformVSlot, + transformChildren, + ], + directiveTransforms: { + bind: transformVBind, + }, +}) + +describe('compiler: transition', () => { + test('basic', () => { + const { code } = compileWithElementTransform( + `

foo

`, + ) + expect(code).toMatchSnapshot() + }) + + test('work with v-if', () => { + const { code } = compileWithElementTransform( + `

foo

`, + ) + + expect(code).toMatchSnapshot() + // n2 should have a key + expect(code).contains('n2.key = 2') + }) + + test('work with dynamic keyed children', () => { + const { code } = compileWithElementTransform( + ` +

foo

+
`, + ) + + expect(code).toMatchSnapshot() + expect(code).contains('_createKeyedFragment(() => _ctx.key') + // should preserve key + expect(code).contains('n0.key = _ctx.key') + }) +}) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap new file mode 100644 index 00000000000..d515ed7a288 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap @@ -0,0 +1,53 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: transition > basic 1`] = ` +"import { VaporTransition as _VaporTransition, createComponent as _createComponent, template as _template } from 'vue'; +const t0 = _template("

foo

") + +export function render(_ctx) { + const n1 = _createComponent(_VaporTransition, { appear: () => ("") }, { + "default": () => { + const n0 = t0() + return n0 + } + }, true) + return n1 +}" +`; + +exports[`compiler: transition > work with dynamic keyed children 1`] = ` +"import { VaporTransition as _VaporTransition, createKeyedFragment as _createKeyedFragment, createComponent as _createComponent, template as _template } from 'vue'; +const t0 = _template("

foo

") + +export function render(_ctx) { + const n1 = _createComponent(_VaporTransition, null, { + "default": () => { + return _createKeyedFragment(() => _ctx.key, () => { + const n0 = t0() + n0.key = _ctx.key + return n0 + }) + } + }, true) + return n1 +}" +`; + +exports[`compiler: transition > work with v-if 1`] = ` +"import { VaporTransition as _VaporTransition, createIf as _createIf, createComponent as _createComponent, template as _template } from 'vue'; +const t0 = _template("

foo

") + +export function render(_ctx) { + const n3 = _createComponent(_VaporTransition, null, { + "default": () => { + const n0 = _createIf(() => (_ctx.show), () => { + const n2 = t0() + n2.key = 2 + return n2 + }) + return n0 + } + }, true) + return n3 +}" +`; From b65db59169fbe5efe559f168640d4bf2ffafbdaf Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 6 Mar 2025 17:40:52 +0800 Subject: [PATCH 23/62] wip: add vapor transition e2e tests --- .../__tests__/transition.spec.ts | 238 ++++++++++++++++++ packages-private/vapor-e2e-test/index.html | 1 + .../vapor-e2e-test/transition/App.vue | 47 ++++ .../transition/components/VaporCompA.vue | 6 + .../transition/components/VaporCompB.vue | 6 + .../vapor-e2e-test/transition/index.html | 2 + .../vapor-e2e-test/transition/main.ts | 5 + .../vapor-e2e-test/transition/style.css | 19 ++ .../vapor-e2e-test/vite.config.ts | 1 + 9 files changed, 325 insertions(+) create mode 100644 packages-private/vapor-e2e-test/__tests__/transition.spec.ts create mode 100644 packages-private/vapor-e2e-test/transition/App.vue create mode 100644 packages-private/vapor-e2e-test/transition/components/VaporCompA.vue create mode 100644 packages-private/vapor-e2e-test/transition/components/VaporCompB.vue create mode 100644 packages-private/vapor-e2e-test/transition/index.html create mode 100644 packages-private/vapor-e2e-test/transition/main.ts create mode 100644 packages-private/vapor-e2e-test/transition/style.css diff --git a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts new file mode 100644 index 00000000000..922ff0d0eaf --- /dev/null +++ b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts @@ -0,0 +1,238 @@ +import path from 'node:path' +import { + E2E_TIMEOUT, + setupPuppeteer, +} from '../../../packages/vue/__tests__/e2e/e2eUtils' +import connect from 'connect' +import sirv from 'sirv' +const { + page, + click, + classList, + text, + nextFrame, + timeout, + isVisible, + count, + html, +} = setupPuppeteer() + +const duration = process.env.CI ? 200 : 50 +const buffer = process.env.CI ? 50 : 20 +const transitionFinish = (time = duration) => timeout(time + buffer) + +describe('vapor transition', () => { + let server: any + const port = '8195' + beforeAll(() => { + server = connect() + .use(sirv(path.resolve(import.meta.dirname, '../dist'))) + .listen(port) + process.on('SIGTERM', () => server && server.close()) + }) + + afterAll(() => { + server.close() + }) + + beforeEach(async () => { + const baseUrl = `http://localhost:${port}/transition/` + await page().goto(baseUrl) + await page().waitForSelector('#app') + }) + + const classWhenTransitionStart = async ( + btnSelector: string, + containerSelector: string, + ) => { + return page().evaluate( + ([btnSel, containerSel]) => { + ;(document.querySelector(btnSel) as HTMLElement)!.click() + return Promise.resolve().then(() => { + return document.querySelector(containerSel)!.className.split(/\s+/g) + }) + }, + [btnSelector, containerSelector], + ) + } + + test( + 'should work with v-show', + async () => { + const btnSelector = '.vshow > button' + const containerSelector = '.vshow > h1' + + expect(await text(containerSelector)).toContain('vShow') + + // leave + expect( + await classWhenTransitionStart(btnSelector, containerSelector), + ).toStrictEqual(['v-leave-from', 'v-leave-active']) + + await nextFrame() + expect(await classList(containerSelector)).toStrictEqual([ + 'v-leave-active', + 'v-leave-to', + ]) + + await transitionFinish() + expect(await isVisible(containerSelector)).toBe(false) + + // enter + expect( + await classWhenTransitionStart(btnSelector, containerSelector), + ).toStrictEqual(['v-enter-from', 'v-enter-active']) + + await nextFrame() + expect(await classList(containerSelector)).toStrictEqual([ + 'v-enter-active', + 'v-enter-to', + ]) + + await transitionFinish() + expect(await isVisible(containerSelector)).toBe(true) + }, + E2E_TIMEOUT, + ) + + test( + 'should work with v-if + appear', + async () => { + const btnSelector = '.vif > button' + const containerSelector = '.vif > h1' + + // appear + expect(await classList(containerSelector)).toStrictEqual([ + 'v-enter-from', + 'v-enter-active', + ]) + expect(await text(containerSelector)).toContain('vIf') + await transitionFinish() + + // leave + expect( + await classWhenTransitionStart(btnSelector, containerSelector), + ).toStrictEqual(['v-leave-from', 'v-leave-active']) + + await nextFrame() + expect(await classList(containerSelector)).toStrictEqual([ + 'v-leave-active', + 'v-leave-to', + ]) + + await transitionFinish() + expect(await count(containerSelector)).toBe(0) + + // enter + expect( + await classWhenTransitionStart(btnSelector, containerSelector), + ).toStrictEqual(['v-enter-from', 'v-enter-active']) + + await nextFrame() + expect(await classList(containerSelector)).toStrictEqual([ + 'v-enter-active', + 'v-enter-to', + ]) + + await transitionFinish() + expect(await isVisible(containerSelector)).toBe(true) + }, + E2E_TIMEOUT, + ) + + test( + 'should work with keyed element', + async () => { + const btnSelector = '.keyed > button' + const containerSelector = '.keyed > h1' + + expect(await text(containerSelector)).toContain('0') + + // change key + expect( + await classWhenTransitionStart(btnSelector, containerSelector), + ).toStrictEqual(['v-leave-from', 'v-leave-active']) + + await nextFrame() + expect(await classList(containerSelector)).toStrictEqual([ + 'v-leave-active', + 'v-leave-to', + ]) + + await transitionFinish() + expect(await text(containerSelector)).toContain('1') + + // change key again + expect( + await classWhenTransitionStart(btnSelector, containerSelector), + ).toStrictEqual(['v-leave-from', 'v-leave-active']) + + await nextFrame() + expect(await classList(containerSelector)).toStrictEqual([ + 'v-leave-active', + 'v-leave-to', + ]) + + await transitionFinish() + expect(await text(containerSelector)).toContain('2') + }, + E2E_TIMEOUT, + ) + + test( + 'should work with out-in mode', + async () => { + const btnSelector = '.out-in > button' + const containerSelector = '.out-in > div' + + expect(await html(containerSelector)).toBe(`
vapor compB
`) + + // compB -> compA + await click(btnSelector) + expect(await html(containerSelector)).toBe( + `
vapor compB
`, + ) + + await nextFrame() + expect(await html(containerSelector)).toBe( + `
vapor compB
`, + ) + + await transitionFinish() + await nextFrame() + expect(await html(containerSelector)).toBe( + `
vapor compA
`, + ) + + await transitionFinish() + expect(await html(containerSelector)).toBe( + `
vapor compA
`, + ) + + // compA -> compB + await click(btnSelector) + expect(await html(containerSelector)).toBe( + `
vapor compA
`, + ) + + await nextFrame() + expect(await html(containerSelector)).toBe( + `
vapor compA
`, + ) + + await transitionFinish() + await nextFrame() + expect(await html(containerSelector)).toBe( + `
vapor compB
`, + ) + + await transitionFinish() + expect(await html(containerSelector)).toBe( + `
vapor compB
`, + ) + }, + E2E_TIMEOUT, + ) + + test.todo('should work with in-out mode', async () => {}, E2E_TIMEOUT) +}) diff --git a/packages-private/vapor-e2e-test/index.html b/packages-private/vapor-e2e-test/index.html index 7dc205e5ab0..160e2125d33 100644 --- a/packages-private/vapor-e2e-test/index.html +++ b/packages-private/vapor-e2e-test/index.html @@ -1,2 +1,3 @@ VDOM / Vapor interop Vapor TodoMVC +Vapor Transition diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue new file mode 100644 index 00000000000..6d2ebd0506f --- /dev/null +++ b/packages-private/vapor-e2e-test/transition/App.vue @@ -0,0 +1,47 @@ + + + + \ No newline at end of file diff --git a/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue b/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue new file mode 100644 index 00000000000..24c98ecb630 --- /dev/null +++ b/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue b/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue new file mode 100644 index 00000000000..8064165f3f1 --- /dev/null +++ b/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/packages-private/vapor-e2e-test/transition/index.html b/packages-private/vapor-e2e-test/transition/index.html new file mode 100644 index 00000000000..79052a023ba --- /dev/null +++ b/packages-private/vapor-e2e-test/transition/index.html @@ -0,0 +1,2 @@ + +
diff --git a/packages-private/vapor-e2e-test/transition/main.ts b/packages-private/vapor-e2e-test/transition/main.ts new file mode 100644 index 00000000000..d02bb979065 --- /dev/null +++ b/packages-private/vapor-e2e-test/transition/main.ts @@ -0,0 +1,5 @@ +import { createVaporApp } from 'vue' +import App from './App.vue' +import './style.css' + +createVaporApp(App).mount('#app') diff --git a/packages-private/vapor-e2e-test/transition/style.css b/packages-private/vapor-e2e-test/transition/style.css new file mode 100644 index 00000000000..3f1cce3dc75 --- /dev/null +++ b/packages-private/vapor-e2e-test/transition/style.css @@ -0,0 +1,19 @@ +.v-enter-active, +.v-leave-active { + transition: opacity 50ms ease; +} + +.v-enter-from, +.v-leave-to { + opacity: 0; +} + +.fade-enter-active, +.fade-leave-active { + transition: opacity 50ms ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} \ No newline at end of file diff --git a/packages-private/vapor-e2e-test/vite.config.ts b/packages-private/vapor-e2e-test/vite.config.ts index 1e29a4dbd13..846620ad017 100644 --- a/packages-private/vapor-e2e-test/vite.config.ts +++ b/packages-private/vapor-e2e-test/vite.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ input: { interop: resolve(import.meta.dirname, 'interop/index.html'), todomvc: resolve(import.meta.dirname, 'todomvc/index.html'), + transition: resolve(import.meta.dirname, 'transition/index.html'), }, }, }, From 0f3dffc23c88478687311d2e20a42377ae647752 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 09:41:57 +0000 Subject: [PATCH 24/62] [autofix.ci] apply automated fixes --- packages-private/vapor-e2e-test/transition/App.vue | 7 ++++--- .../vapor-e2e-test/transition/components/VaporCompA.vue | 4 ++-- .../vapor-e2e-test/transition/components/VaporCompB.vue | 4 ++-- packages-private/vapor-e2e-test/transition/style.css | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue index 6d2ebd0506f..008506800e0 100644 --- a/packages-private/vapor-e2e-test/transition/App.vue +++ b/packages-private/vapor-e2e-test/transition/App.vue @@ -8,7 +8,8 @@ import VaporCompA from './components/VaporCompA.vue' import VaporCompB from './components/VaporCompB.vue' const activeComponent = shallowRef(VaporCompA) function toggleComponent() { - activeComponent.value = activeComponent.value === VaporCompA ? VaporCompB : VaporCompA + activeComponent.value = + activeComponent.value === VaporCompA ? VaporCompB : VaporCompA } @@ -42,6 +43,6 @@ function toggleComponent() { \ No newline at end of file + diff --git a/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue b/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue index 24c98ecb630..c22f3b6bbc5 100644 --- a/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue +++ b/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue @@ -2,5 +2,5 @@ const msg = 'vapor compB' \ No newline at end of file +
{{ msg }}
+ diff --git a/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue b/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue index 8064165f3f1..40b09eaf45b 100644 --- a/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue +++ b/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue @@ -2,5 +2,5 @@ const msg = 'vapor compA' \ No newline at end of file +
{{ msg }}
+ diff --git a/packages-private/vapor-e2e-test/transition/style.css b/packages-private/vapor-e2e-test/transition/style.css index 3f1cce3dc75..98f19c8cd92 100644 --- a/packages-private/vapor-e2e-test/transition/style.css +++ b/packages-private/vapor-e2e-test/transition/style.css @@ -16,4 +16,4 @@ .fade-enter-from, .fade-leave-to { opacity: 0; -} \ No newline at end of file +} From 205858e8be128879322e841c1b4a28d4067ee580 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 6 Mar 2025 20:59:35 +0800 Subject: [PATCH 25/62] wip: test --- .../__tests__/transition.spec.ts | 19 +++++++++++++++++++ .../src/transforms/transformElement.ts | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts index 922ff0d0eaf..44065e6c4bf 100644 --- a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts @@ -199,6 +199,13 @@ describe('vapor transition', () => { ) await transitionFinish() + expect(await html(containerSelector)).toBe(`
vapor compA
`) + + await nextFrame() + expect(await html(containerSelector)).toBe( + `
vapor compA
`, + ) + await nextFrame() expect(await html(containerSelector)).toBe( `
vapor compA
`, @@ -235,4 +242,16 @@ describe('vapor transition', () => { ) test.todo('should work with in-out mode', async () => {}, E2E_TIMEOUT) + + test.todo('transition hooks', async () => {}, E2E_TIMEOUT) + + describe('interop', () => { + test.todo('interop: render vdom component', async () => {}, E2E_TIMEOUT) + + test.todo( + 'interop: switch between vdom/vapor component', + async () => {}, + E2E_TIMEOUT, + ) + }) }) diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 35fd596ee83..370cfdc993e 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -130,7 +130,9 @@ function transformComponentElement( } context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT - context.registerOperation({ + // context.registerOperation() + // TODO revert wait for https://github.com/vuejs/core/pull/12951 get merged + context.block.operation.unshift({ type: IRNodeTypes.CREATE_COMPONENT_NODE, id: context.reference(), tag, From 0d950d41b5d54251aca1fd3b2f0c68a26b07d9fd Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 6 Mar 2025 22:09:56 +0800 Subject: [PATCH 26/62] wip: save --- .../__tests__/transition.spec.ts | 83 +++++++++++++++---- .../vapor-e2e-test/transition/App.vue | 10 ++- .../src/transforms/transformElement.ts | 4 +- .../src/components/Transition.ts | 1 + 4 files changed, 78 insertions(+), 20 deletions(-) diff --git a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts index 44065e6c4bf..d7d35f836f5 100644 --- a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts @@ -49,7 +49,11 @@ describe('vapor transition', () => { ([btnSel, containerSel]) => { ;(document.querySelector(btnSel) as HTMLElement)!.click() return Promise.resolve().then(() => { - return document.querySelector(containerSel)!.className.split(/\s+/g) + const container = document.querySelector(containerSel)! + return { + classNames: container.className.split(/\s+/g), + innerHTML: container.innerHTML, + } }) }, [btnSelector, containerSelector], @@ -66,7 +70,8 @@ describe('vapor transition', () => { // leave expect( - await classWhenTransitionStart(btnSelector, containerSelector), + (await classWhenTransitionStart(btnSelector, containerSelector)) + .classNames, ).toStrictEqual(['v-leave-from', 'v-leave-active']) await nextFrame() @@ -80,7 +85,8 @@ describe('vapor transition', () => { // enter expect( - await classWhenTransitionStart(btnSelector, containerSelector), + (await classWhenTransitionStart(btnSelector, containerSelector)) + .classNames, ).toStrictEqual(['v-enter-from', 'v-enter-active']) await nextFrame() @@ -102,16 +108,14 @@ describe('vapor transition', () => { const containerSelector = '.vif > h1' // appear - expect(await classList(containerSelector)).toStrictEqual([ - 'v-enter-from', - 'v-enter-active', - ]) + expect(await classList(containerSelector)).contains('v-enter-active') expect(await text(containerSelector)).toContain('vIf') await transitionFinish() // leave expect( - await classWhenTransitionStart(btnSelector, containerSelector), + (await classWhenTransitionStart(btnSelector, containerSelector)) + .classNames, ).toStrictEqual(['v-leave-from', 'v-leave-active']) await nextFrame() @@ -125,7 +129,8 @@ describe('vapor transition', () => { // enter expect( - await classWhenTransitionStart(btnSelector, containerSelector), + (await classWhenTransitionStart(btnSelector, containerSelector)) + .classNames, ).toStrictEqual(['v-enter-from', 'v-enter-active']) await nextFrame() @@ -150,7 +155,8 @@ describe('vapor transition', () => { // change key expect( - await classWhenTransitionStart(btnSelector, containerSelector), + (await classWhenTransitionStart(btnSelector, containerSelector)) + .classNames, ).toStrictEqual(['v-leave-from', 'v-leave-active']) await nextFrame() @@ -164,7 +170,8 @@ describe('vapor transition', () => { // change key again expect( - await classWhenTransitionStart(btnSelector, containerSelector), + (await classWhenTransitionStart(btnSelector, containerSelector)) + .classNames, ).toStrictEqual(['v-leave-from', 'v-leave-active']) await nextFrame() @@ -188,10 +195,10 @@ describe('vapor transition', () => { expect(await html(containerSelector)).toBe(`
vapor compB
`) // compB -> compA - await click(btnSelector) - expect(await html(containerSelector)).toBe( - `
vapor compB
`, - ) + expect( + (await classWhenTransitionStart(btnSelector, containerSelector)) + .innerHTML, + ).toBe(`
vapor compB
`) await nextFrame() expect(await html(containerSelector)).toBe( @@ -241,7 +248,51 @@ describe('vapor transition', () => { E2E_TIMEOUT, ) - test.todo('should work with in-out mode', async () => {}, E2E_TIMEOUT) + test.todo( + 'should work with in-out mode', + async () => { + const btnSelector = '.in-out > button' + const containerSelector = '.in-out > div' + + expect(await html(containerSelector)).toBe(`
vapor compB
`) + + // compA enter + const { innerHTML } = await classWhenTransitionStart( + btnSelector, + containerSelector, + ) + expect(innerHTML).toBe( + `
vapor compB
vapor compA
`, + ) + + await nextFrame() + expect(await html(containerSelector)).toBe( + `
vapor compB
vapor compA
`, + ) + + await transitionFinish() + expect(await html(containerSelector)).toBe( + `
vapor compB
vapor compA
`, + ) + + // compB leave + expect(await html(containerSelector)).toBe( + `
vapor compB
vapor compA
`, + ) + + await nextFrame() + expect(await html(containerSelector)).toBe( + `
vapor compB
vapor compA
`, + ) + + await transitionFinish() + await nextFrame() + expect(await html(containerSelector)).toBe( + `
vapor compA
`, + ) + }, + E2E_TIMEOUT, + ) test.todo('transition hooks', async () => {}, E2E_TIMEOUT) diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue index 008506800e0..3166f274d1b 100644 --- a/packages-private/vapor-e2e-test/transition/App.vue +++ b/packages-private/vapor-e2e-test/transition/App.vue @@ -33,13 +33,21 @@ function toggleComponent() {
- +
+
+ +
+ + + +
+
diff --git a/packages-private/vapor-e2e-test/interop/App.vue b/packages-private/vapor-e2e-test/interop/App.vue index f29df3c80cd..8cf42e47549 100644 --- a/packages-private/vapor-e2e-test/interop/App.vue +++ b/packages-private/vapor-e2e-test/interop/App.vue @@ -3,6 +3,7 @@ import { ref, shallowRef } from 'vue' import VaporComp from './VaporComp.vue' import VaporCompA from '../transition/components/VaporCompA.vue' import VdomComp from '../transition/components/VdomComp.vue' +import VaporSlot from '../transition/components/VaporSlot.vue' const msg = ref('hello') const passSlot = ref(true) @@ -13,6 +14,9 @@ function toggleInteropComponent() { interopComponent.value = interopComponent.value === VaporCompA ? VdomComp : VaporCompA } + +const items = ref(['a', 'b', 'c']) +const enterClick = () => items.value.push('d', 'e') diff --git a/packages-private/vapor-e2e-test/transition-group/App.vue b/packages-private/vapor-e2e-test/transition-group/App.vue new file mode 100644 index 00000000000..55775743c56 --- /dev/null +++ b/packages-private/vapor-e2e-test/transition-group/App.vue @@ -0,0 +1,145 @@ + + + + diff --git a/packages-private/vapor-e2e-test/transition-group/components/VaporComp.vue b/packages-private/vapor-e2e-test/transition-group/components/VaporComp.vue new file mode 100644 index 00000000000..906795d22f2 --- /dev/null +++ b/packages-private/vapor-e2e-test/transition-group/components/VaporComp.vue @@ -0,0 +1,9 @@ + + + diff --git a/packages-private/vapor-e2e-test/transition-group/components/VdomComp.vue b/packages-private/vapor-e2e-test/transition-group/components/VdomComp.vue new file mode 100644 index 00000000000..afd7d55f2be --- /dev/null +++ b/packages-private/vapor-e2e-test/transition-group/components/VdomComp.vue @@ -0,0 +1,9 @@ + + + diff --git a/packages-private/vapor-e2e-test/transition-group/index.html b/packages-private/vapor-e2e-test/transition-group/index.html new file mode 100644 index 00000000000..79052a023ba --- /dev/null +++ b/packages-private/vapor-e2e-test/transition-group/index.html @@ -0,0 +1,2 @@ + +
diff --git a/packages-private/vapor-e2e-test/transition-group/main.ts b/packages-private/vapor-e2e-test/transition-group/main.ts new file mode 100644 index 00000000000..efa06a296cc --- /dev/null +++ b/packages-private/vapor-e2e-test/transition-group/main.ts @@ -0,0 +1,5 @@ +import { createVaporApp, vaporInteropPlugin } from 'vue' +import App from './App.vue' +import '../../../packages/vue/__tests__/e2e/style.css' + +createVaporApp(App).use(vaporInteropPlugin).mount('#app') diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue index 057bb0a229e..b8470c10749 100644 --- a/packages-private/vapor-e2e-test/transition/App.vue +++ b/packages-private/vapor-e2e-test/transition/App.vue @@ -26,78 +26,80 @@ function toggleInteropComponent() { @@ -106,3 +108,10 @@ function toggleInteropComponent() { height: 100px; } + diff --git a/packages-private/vapor-e2e-test/transition/components/VaporSlot.vue b/packages-private/vapor-e2e-test/transition/components/VaporSlot.vue new file mode 100644 index 00000000000..f5eff0100f8 --- /dev/null +++ b/packages-private/vapor-e2e-test/transition/components/VaporSlot.vue @@ -0,0 +1,8 @@ + + diff --git a/packages-private/vapor-e2e-test/transition/main.ts b/packages-private/vapor-e2e-test/transition/main.ts index 88bfe0ee7ea..e77d51d1c03 100644 --- a/packages-private/vapor-e2e-test/transition/main.ts +++ b/packages-private/vapor-e2e-test/transition/main.ts @@ -1,5 +1,6 @@ import { createVaporApp, vaporInteropPlugin } from 'vue' import App from './App.vue' +import '../../../packages/vue/__tests__/e2e/style.css' import './style.css' createVaporApp(App).use(vaporInteropPlugin).mount('#app') diff --git a/packages-private/vapor-e2e-test/transition/style.css b/packages-private/vapor-e2e-test/transition/style.css index 98f19c8cd92..e6faf6cea53 100644 --- a/packages-private/vapor-e2e-test/transition/style.css +++ b/packages-private/vapor-e2e-test/transition/style.css @@ -17,3 +17,19 @@ .fade-leave-to { opacity: 0; } + +.test-move, +.test-enter-active, +.test-leave-active { + transition: all 50ms cubic-bezier(0.55, 0, 0.1, 1); +} + +.test-enter-from, +.test-leave-to { + opacity: 0; + transform: scaleY(0.01) translate(30px, 0); +} + +.test-leave-active { + position: absolute; +} diff --git a/packages-private/vapor-e2e-test/vite.config.ts b/packages-private/vapor-e2e-test/vite.config.ts index 846620ad017..f50fccea3ce 100644 --- a/packages-private/vapor-e2e-test/vite.config.ts +++ b/packages-private/vapor-e2e-test/vite.config.ts @@ -15,6 +15,10 @@ export default defineConfig({ interop: resolve(import.meta.dirname, 'interop/index.html'), todomvc: resolve(import.meta.dirname, 'todomvc/index.html'), transition: resolve(import.meta.dirname, 'transition/index.html'), + transitionGroup: resolve( + import.meta.dirname, + 'transition-group/index.html', + ), }, }, }, diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 35fd596ee83..0ad4ef092fb 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -413,7 +413,9 @@ function dedupeProperties(results: DirectiveTransformResult[]): IRProp[] { } const name = prop.key.content const existing = knownProps.get(name) - if (existing) { + // prop names and event handler names can be the same but serve different purposes + // e.g. `:appear="true"` is a prop while `@appear="handler"` is an event handler + if (existing && existing.handler === prop.handler) { if (name === 'style' || name === 'class') { mergePropValues(existing, prop) } diff --git a/packages/compiler-vapor/src/utils.ts b/packages/compiler-vapor/src/utils.ts index d390c69a21a..2d5ba72b39e 100644 --- a/packages/compiler-vapor/src/utils.ts +++ b/packages/compiler-vapor/src/utils.ts @@ -94,20 +94,35 @@ export function isInTransition( context: TransformContext, ): boolean { const parentNode = context.parent && context.parent.node - return !!(parentNode && isTransitionNode(parentNode as ElementNode)) + return !!( + parentNode && + (isTransitionNode(parentNode as ElementNode) || + isTransitionGroupNode(parentNode as ElementNode)) + ) } export function isTransitionNode(node: ElementNode): boolean { return node.type === NodeTypes.ELEMENT && isTransitionTag(node.tag) } +export function isTransitionGroupNode(node: ElementNode): boolean { + return node.type === NodeTypes.ELEMENT && isTransitionGroupTag(node.tag) +} + export function isTransitionTag(tag: string): boolean { tag = tag.toLowerCase() return tag === 'transition' || tag === 'vaportransition' } +export function isTransitionGroupTag(tag: string): boolean { + tag = tag.toLowerCase().replace(/-/g, '') + return tag === 'transitiongroup' || tag === 'vaportransitiongroup' +} + export function isBuiltInComponent(tag: string): string | undefined { if (isTransitionTag(tag)) { return 'VaporTransition' + } else if (isTransitionGroupTag(tag)) { + return 'VaporTransitionGroup' } } diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts index 8400e71d6c0..4f4993b5ce1 100644 --- a/packages/runtime-dom/src/components/TransitionGroup.ts +++ b/packages/runtime-dom/src/components/TransitionGroup.ts @@ -32,7 +32,7 @@ import { extend } from '@vue/shared' const positionMap = new WeakMap() const newPositionMap = new WeakMap() -const moveCbKey = Symbol('_moveCb') +export const moveCbKey: symbol = Symbol('_moveCb') const enterCbKey = Symbol('_enterCb') export type TransitionGroupProps = Omit & { @@ -87,7 +87,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ // we divide the work into three loops to avoid mixing DOM reads and writes // in each iteration - which helps prevent layout thrashing. - prevChildren.forEach(callPendingCbs) + prevChildren.forEach(vnode => callPendingCbs(vnode.el)) prevChildren.forEach(recordPosition) const movedChildren = prevChildren.filter(applyTranslation) @@ -96,20 +96,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ movedChildren.forEach(c => { const el = c.el as ElementWithTransition - const style = el.style - addTransitionClass(el, moveClass) - style.transform = style.webkitTransform = style.transitionDuration = '' - const cb = ((el as any)[moveCbKey] = (e: TransitionEvent) => { - if (e && e.target !== el) { - return - } - if (!e || /transform$/.test(e.propertyName)) { - el.removeEventListener('transitionend', cb) - ;(el as any)[moveCbKey] = null - removeTransitionClass(el, moveClass) - } - }) - el.addEventListener('transitionend', cb) + handleMovedChildren(el, moveClass) }) }) @@ -177,8 +164,7 @@ export const TransitionGroup = TransitionGroupImpl as unknown as { } } -function callPendingCbs(c: VNode) { - const el = c.el as any +export function callPendingCbs(el: any): void { if (el[moveCbKey]) { el[moveCbKey]() } @@ -192,19 +178,34 @@ function recordPosition(c: VNode) { } function applyTranslation(c: VNode): VNode | undefined { - const oldPos = positionMap.get(c)! - const newPos = newPositionMap.get(c)! + if ( + baseApplyTranslation( + positionMap.get(c)!, + newPositionMap.get(c)!, + c.el as ElementWithTransition, + ) + ) { + return c + } +} + +export function baseApplyTranslation( + oldPos: DOMRect, + newPos: DOMRect, + el: ElementWithTransition, +): boolean { const dx = oldPos.left - newPos.left const dy = oldPos.top - newPos.top if (dx || dy) { - const s = (c.el as HTMLElement).style + const s = (el as HTMLElement).style s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)` s.transitionDuration = '0s' - return c + return true } + return false } -function hasCSSTransform( +export function hasCSSTransform( el: ElementWithTransition, root: Node, moveClass: string, @@ -231,3 +232,23 @@ function hasCSSTransform( container.removeChild(clone) return hasTransform } + +export const handleMovedChildren = ( + el: ElementWithTransition, + moveClass: string, +): void => { + const style = el.style + addTransitionClass(el, moveClass) + style.transform = style.webkitTransform = style.transitionDuration = '' + const cb = ((el as any)[moveCbKey] = (e: TransitionEvent) => { + if (e && e.target !== el) { + return + } + if (!e || /transform$/.test(e.propertyName)) { + el.removeEventListener('transitionend', cb) + ;(el as any)[moveCbKey] = null + removeTransitionClass(el, moveClass) + } + }) + el.addEventListener('transitionend', cb) +} diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 450ec74d15f..521bb46498a 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -271,10 +271,22 @@ export { useCssModule } from './helpers/useCssModule' export { useCssVars } from './helpers/useCssVars' // DOM-only components -export { Transition, type TransitionProps } from './components/Transition' +export { + Transition, + type TransitionProps, + forceReflow, + addTransitionClass, + removeTransitionClass, +} from './components/Transition' +export type { ElementWithTransition } from './components/Transition' export { TransitionGroup, type TransitionGroupProps, + hasCSSTransform, + callPendingCbs, + moveCbKey, + handleMovedChildren, + baseApplyTranslation, } from './components/TransitionGroup' // **Internal** DOM-only runtime directive helpers diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 19653cd5daa..1e4be0b5163 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -22,6 +22,7 @@ import { currentInstance, isVaporComponent } from './component' import type { DynamicSlot } from './componentSlots' import { renderEffect } from './renderEffect' import { VaporVForFlags } from '../../shared/src/vaporFlags' +import { applyTransitionEnterHooks } from './components/Transition' class ForBlock extends VaporFragment { scope: EffectScope | undefined @@ -315,6 +316,10 @@ export const createFor = ( getKey && getKey(item, key, index), )) + if (frag.$transition) { + applyTransitionEnterHooks(block.nodes, frag.$transition) + } + if (parent) insert(block.nodes, parent, anchor) return block @@ -415,8 +420,8 @@ function getItem( } } -function normalizeAnchor(node: Block): Node { - if (node instanceof Node) { +function normalizeAnchor(node: Block): Node | undefined { + if (node && node instanceof Node) { return node } else if (isArray(node)) { return normalizeAnchor(node[0]) @@ -439,3 +444,7 @@ export function getRestElement(val: any, keys: string[]): any { export function getDefaultValue(val: any, defaultVal: any): any { return val === undefined ? defaultVal : val } + +export function isForBlock(block: Block): block is ForBlock { + return block instanceof ForBlock +} diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 6c904ab8690..26c0d8ca379 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -28,6 +28,7 @@ export interface TransitionOptions { export interface VaporTransitionHooks extends TransitionHooks { state: TransitionState props: TransitionProps + disabledOnMoving?: boolean } export type TransitionBlock = @@ -157,7 +158,11 @@ export function insert( if (block instanceof Node) { if (!isHydrating) { // don't apply transition on text or comment nodes - if ((block as TransitionBlock).$transition && block instanceof Element) { + if ( + block instanceof Element && + (block as TransitionBlock).$transition && + !(block as TransitionBlock).$transition!.disabledOnMoving + ) { performTransitionEnter( block, (block as TransitionBlock).$transition as TransitionHooks, diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index d262c472382..fbba29f3ba9 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -21,6 +21,7 @@ import { isFragment, } from '../block' import { type VaporComponentInstance, isVaporComponent } from '../component' +import { isArray } from '@vue/shared' const decorate = (t: typeof VaporTransition) => { t.displayName = 'VaporTransition' @@ -93,7 +94,7 @@ const getTransitionHooksContext = ( return context } -function resolveTransitionHooks( +export function resolveTransitionHooks( block: TransitionBlock, props: TransitionProps, state: TransitionState, @@ -118,10 +119,10 @@ function resolveTransitionHooks( return hooks } -function setTransitionHooks( +export function setTransitionHooks( block: TransitionBlock, hooks: VaporTransitionHooks, -) { +): void { block.$transition = hooks } @@ -144,7 +145,7 @@ export function applyTransitionEnterHooks( setTransitionHooks(child, enterHooks) if (isFragment(block)) { // also set transition hooks on fragment for reusing during it's updating - setTransitionHooks(block, enterHooks) + setTransitionHooksToFragment(block, enterHooks) } return enterHooks } @@ -211,7 +212,7 @@ export function findTransitionBlock(block: Block): TransitionBlock | undefined { } else if (isVaporComponent(block)) { child = findTransitionBlock(block.block) if (child && child.$key === undefined) child.$key = block.type.__name - } else if (Array.isArray(block)) { + } else if (isArray(block)) { child = block[0] as TransitionBlock let hasFound = false for (const c of block) { @@ -254,3 +255,16 @@ export function setTransitionToInstance( setTransitionHooks(child, hooks) } + +export function setTransitionHooksToFragment( + block: Block, + hooks: VaporTransitionHooks, +): void { + if (isFragment(block)) { + setTransitionHooks(block, hooks) + } else if (isArray(block)) { + for (let i = 0; i < block.length; i++) { + setTransitionHooksToFragment(block[i], hooks) + } + } +} diff --git a/packages/runtime-vapor/src/components/TransitionGroup.ts b/packages/runtime-vapor/src/components/TransitionGroup.ts new file mode 100644 index 00000000000..35d39c66404 --- /dev/null +++ b/packages/runtime-vapor/src/components/TransitionGroup.ts @@ -0,0 +1,209 @@ +import { + type ElementWithTransition, + type TransitionGroupProps, + TransitionPropsValidators, + baseApplyTranslation, + callPendingCbs, + currentInstance, + forceReflow, + handleMovedChildren, + hasCSSTransform, + onBeforeUpdate, + onUpdated, + resolveTransitionProps, + useTransitionState, + warn, +} from '@vue/runtime-dom' +import { extend, isArray } from '@vue/shared' +import { + type Block, + DynamicFragment, + type TransitionBlock, + type VaporTransitionHooks, + insert, + isFragment, +} from '../block' +import { + resolveTransitionHooks, + setTransitionHooks, + setTransitionHooksToFragment, +} from './Transition' +import { type ObjectVaporComponent, isVaporComponent } from '../component' +import { isForBlock } from '../apiCreateFor' +import { renderEffect, setDynamicProps } from '@vue/runtime-vapor' + +const positionMap = new WeakMap() +const newPositionMap = new WeakMap() + +const decorate = (t: typeof VaporTransitionGroup) => { + delete (t.props! as any).mode + t.__vapor = true + return t +} + +export const VaporTransitionGroup: ObjectVaporComponent = decorate({ + name: 'VaporTransitionGroup', + + props: /*@__PURE__*/ extend({}, TransitionPropsValidators, { + tag: String, + moveClass: String, + }), + + setup(props: TransitionGroupProps, { slots }: any) { + const instance = currentInstance + const state = useTransitionState() + const cssTransitionProps = resolveTransitionProps(props) + + let prevChildren: TransitionBlock[] + let children: TransitionBlock[] + let slottedBlock: Block + + onBeforeUpdate(() => { + prevChildren = [] + children = getTransitionBlocks(slottedBlock) + if (children) { + for (let i = 0; i < children.length; i++) { + const child = children[i] + if (isValidTransitionBlock(child)) { + prevChildren.push(child) + const hook = (child as TransitionBlock).$transition! + // disabled transition during moving, so the children will be + // inserted into the correct position immediately. this prevents + // `recordPosition` from getting incorrect positions in `onUpdated` + hook.disabledOnMoving = true + positionMap.set(child, getEl(child).getBoundingClientRect()) + } + } + } + }) + + onUpdated(() => { + if (!prevChildren.length) { + return + } + + const moveClass = props.moveClass || `${props.name || 'v'}-move` + + const firstChild = findFirstChild(prevChildren) + if ( + !firstChild || + !hasCSSTransform( + firstChild as ElementWithTransition, + firstChild.parentNode as Node, + moveClass, + ) + ) { + return + } + + prevChildren.forEach(callPendingCbs) + prevChildren.forEach(child => { + delete child.$transition!.disabledOnMoving + recordPosition(child) + }) + const movedChildren = prevChildren.filter(applyTranslation) + + // force reflow to put everything in position + forceReflow() + + movedChildren.forEach(c => + handleMovedChildren(getEl(c) as ElementWithTransition, moveClass), + ) + }) + + slottedBlock = slots.default && slots.default() + + // store props and state on fragment for reusing during insert new items + setTransitionHooksToFragment(slottedBlock, { + props: cssTransitionProps, + state, + } as VaporTransitionHooks) + + children = getTransitionBlocks(slottedBlock) + for (let i = 0; i < children.length; i++) { + const child = children[i] + if (isValidTransitionBlock(child)) { + if ((child as TransitionBlock).$key != null) { + setTransitionHooks( + child, + resolveTransitionHooks(child, cssTransitionProps, state, instance!), + ) + } else if (__DEV__ && (child as TransitionBlock).$key == null) { + warn(` children must be keyed`) + } + } + } + + const tag = props.tag + if (tag) { + const el = document.createElement(tag) + insert(slottedBlock, el) + // fallthrough attrs + renderEffect(() => setDynamicProps(el, [instance!.attrs])) + return [el] + } else { + const frag = __DEV__ + ? new DynamicFragment('transitionGroup') + : new DynamicFragment() + renderEffect(() => frag.update(() => slottedBlock)) + return frag + } + }, +}) + +function getTransitionBlocks(block: Block) { + let children: TransitionBlock[] = [] + if (block instanceof Node) { + children.push(block) + } else if (isVaporComponent(block)) { + children.push(...getTransitionBlocks(block.block)) + } else if (isArray(block)) { + for (let i = 0; i < block.length; i++) { + const b = block[i] + const blocks = getTransitionBlocks(b) + if (isForBlock(b)) blocks.forEach(block => (block.$key = b.key)) + children.push(...blocks) + } + } else if (isFragment(block)) { + if (block.insert) { + // vdom component + children.push(block) + } else { + children.push(...getTransitionBlocks(block.nodes)) + } + } + + return children +} + +function isValidTransitionBlock(block: Block): boolean { + return !!(block instanceof Element || (isFragment(block) && block.insert)) +} + +function getEl(c: TransitionBlock): Element { + return (isFragment(c) ? c.nodes : c) as Element +} + +function recordPosition(c: TransitionBlock) { + newPositionMap.set(c, getEl(c).getBoundingClientRect()) +} + +function applyTranslation(c: TransitionBlock): TransitionBlock | undefined { + if ( + baseApplyTranslation( + positionMap.get(c)!, + newPositionMap.get(c)!, + getEl(c) as ElementWithTransition, + ) + ) { + return c + } +} + +function findFirstChild(children: TransitionBlock[]): Element | undefined { + for (let i = 0; i < children.length; i++) { + const child = children[i] + const el = getEl(child) + if (el.isConnected) return el + } +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 1a23a97a3c6..df7810404b5 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -43,3 +43,4 @@ export { } from './directives/vModel' export { withVaporDirectives } from './directives/custom' export { VaporTransition } from './components/Transition' +export { VaporTransitionGroup } from './components/TransitionGroup' diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 0b17b472922..b9bce9d6f8a 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -35,7 +35,7 @@ import { insert, remove, } from './block' -import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' +import { EMPTY_OBJ, extend, isFunction, isReservedProp } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' import { renderEffect } from './renderEffect' @@ -54,7 +54,15 @@ const vaporInteropImpl: Omit< const prev = currentInstance simpleSetCurrentInstance(parentComponent) - const propsRef = shallowRef(vnode.props) + // filter out reserved props + const props: VNode['props'] = {} + for (const key in vnode.props) { + if (!isReservedProp(key)) { + props[key] = vnode.props[key] + } + } + + const propsRef = shallowRef(props) const slotsRef = shallowRef(vnode.children) // @ts-expect-error @@ -221,6 +229,7 @@ function createVDOMComponent( parentInstance as any, ) } + frag.nodes = vnode.el as Node simpleSetCurrentInstance(prev) } diff --git a/packages/vue/__tests__/e2e/style.css b/packages/vue/__tests__/e2e/style.css new file mode 100644 index 00000000000..ae6749b3afb --- /dev/null +++ b/packages/vue/__tests__/e2e/style.css @@ -0,0 +1,77 @@ +.test { + -webkit-transition: opacity 50ms ease; + transition: opacity 50ms ease; +} +.group-move { + -webkit-transition: -webkit-transform 50ms ease; + transition: transform 50ms ease; +} +.v-appear, +.v-enter, +.v-leave-active, +.test-appear, +.test-enter, +.test-leave-active, +.test-reflow-enter, +.test-reflow-leave-to, +.hello, +.bye.active, +.changed-enter { + opacity: 0; +} +.test-reflow-leave-active, +.test-reflow-enter-active { + -webkit-transition: opacity 50ms ease; + transition: opacity 50ms ease; +} +.test-reflow-leave-from { + opacity: 0.9; +} +.test-anim-enter-active { + animation: test-enter 50ms; + -webkit-animation: test-enter 50ms; +} +.test-anim-leave-active { + animation: test-leave 50ms; + -webkit-animation: test-leave 50ms; +} +.test-anim-long-enter-active { + animation: test-enter 100ms; + -webkit-animation: test-enter 100ms; +} +.test-anim-long-leave-active { + animation: test-leave 100ms; + -webkit-animation: test-leave 100ms; +} +@keyframes test-enter { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@-webkit-keyframes test-enter { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes test-leave { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +@-webkit-keyframes test-leave { + from { + opacity: 1; + } + to { + opacity: 0; + } +} diff --git a/packages/vue/__tests__/e2e/transition.html b/packages/vue/__tests__/e2e/transition.html index ab404d67dc7..7f5fce9e34a 100644 --- a/packages/vue/__tests__/e2e/transition.html +++ b/packages/vue/__tests__/e2e/transition.html @@ -1,82 +1,4 @@
- + From dbecdf9184e45f71c04b9ad3ec02d242b0de3252 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 12 Mar 2025 09:12:39 +0800 Subject: [PATCH 40/62] wip: save --- packages/compiler-vapor/src/transforms/vIf.ts | 2 +- .../compiler-vapor/src/transforms/vSlot.ts | 6 +++-- packages/compiler-vapor/src/utils.ts | 6 +---- .../src/components/BaseTransition.ts | 1 + packages/runtime-core/src/renderer.ts | 2 ++ .../src/components/TransitionGroup.ts | 3 +++ packages/runtime-vapor/src/apiCreateFor.ts | 1 + packages/runtime-vapor/src/block.ts | 2 +- .../src/components/TransitionGroup.ts | 26 ++++++++++++------- packages/runtime-vapor/src/vdomInterop.ts | 3 ++- 10 files changed, 33 insertions(+), 19 deletions(-) diff --git a/packages/compiler-vapor/src/transforms/vIf.ts b/packages/compiler-vapor/src/transforms/vIf.ts index ad527a899a2..8454099ee90 100644 --- a/packages/compiler-vapor/src/transforms/vIf.ts +++ b/packages/compiler-vapor/src/transforms/vIf.ts @@ -124,7 +124,7 @@ export function createIfBranch( const exitBlock = context.enterBlock(branch) context.reference() // generate key for branch result when it's in transition - // the key will be used to cache node at runtime + // the key will be used to track node leaving at runtime branch.dynamic.needsKey = isInTransition(context) return [branch, exitBlock] } diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 66b24b0a9f0..9a65e6f1bb4 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -253,8 +253,10 @@ function createSlotBlock( ): [SlotBlockIRNode, () => void] { const block: SlotBlockIRNode = newBlock(slotNode) block.props = dir && dir.exp - block.key = key - if (key) block.dynamic.needsKey = true + if (key) { + block.key = key + block.dynamic.needsKey = true + } const exitBlock = context.enterBlock(block) return [block, exitBlock] } diff --git a/packages/compiler-vapor/src/utils.ts b/packages/compiler-vapor/src/utils.ts index 2d5ba72b39e..d2c7eca3bb1 100644 --- a/packages/compiler-vapor/src/utils.ts +++ b/packages/compiler-vapor/src/utils.ts @@ -94,11 +94,7 @@ export function isInTransition( context: TransformContext, ): boolean { const parentNode = context.parent && context.parent.node - return !!( - parentNode && - (isTransitionNode(parentNode as ElementNode) || - isTransitionGroupNode(parentNode as ElementNode)) - ) + return !!(parentNode && isTransitionNode(parentNode as ElementNode)) } export function isTransitionNode(node: ElementNode): boolean { diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 477c545ad65..fde42148304 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -376,6 +376,7 @@ export function resolveTransitionHooks( return baseResolveTransitionHooks(context, props, state, instance) } +// shared between vdom and vapor export function baseResolveTransitionHooks( context: TransitionHooksContext, props: BaseTransitionProps, diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 5b1a5084692..744338b3f45 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -2625,6 +2625,7 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void { } } +// shared between vdom and vapor export function performTransitionEnter( el: RendererElement, transition: TransitionHooks, @@ -2640,6 +2641,7 @@ export function performTransitionEnter( } } +// shared between vdom and vapor export function performTransitionLeave( el: RendererElement, transition: TransitionHooks, diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts index 4f4993b5ce1..8e38dbf0f23 100644 --- a/packages/runtime-dom/src/components/TransitionGroup.ts +++ b/packages/runtime-dom/src/components/TransitionGroup.ts @@ -189,6 +189,7 @@ function applyTranslation(c: VNode): VNode | undefined { } } +// shared between vdom and vapor export function baseApplyTranslation( oldPos: DOMRect, newPos: DOMRect, @@ -205,6 +206,7 @@ export function baseApplyTranslation( return false } +// shared between vdom and vapor export function hasCSSTransform( el: ElementWithTransition, root: Node, @@ -233,6 +235,7 @@ export function hasCSSTransform( return hasTransform } +// shared between vdom and vapor export const handleMovedChildren = ( el: ElementWithTransition, moveClass: string, diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 1e4be0b5163..f6b5d2bf802 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -316,6 +316,7 @@ export const createFor = ( getKey && getKey(item, key, index), )) + // apply transition for new nodes if (frag.$transition) { applyTransitionEnterHooks(block.nodes, frag.$transition) } diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 26c0d8ca379..65f7c4f35d5 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -157,7 +157,7 @@ export function insert( anchor = anchor === 0 ? parent.firstChild : anchor if (block instanceof Node) { if (!isHydrating) { - // don't apply transition on text or comment nodes + // only apply transition on Element nodes if ( block instanceof Element && (block as TransitionBlock).$transition && diff --git a/packages/runtime-vapor/src/components/TransitionGroup.ts b/packages/runtime-vapor/src/components/TransitionGroup.ts index 35d39c66404..93bd202ce2c 100644 --- a/packages/runtime-vapor/src/components/TransitionGroup.ts +++ b/packages/runtime-vapor/src/components/TransitionGroup.ts @@ -71,7 +71,10 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({ // inserted into the correct position immediately. this prevents // `recordPosition` from getting incorrect positions in `onUpdated` hook.disabledOnMoving = true - positionMap.set(child, getEl(child).getBoundingClientRect()) + positionMap.set( + child, + getTransitionElement(child).getBoundingClientRect(), + ) } } } @@ -84,7 +87,7 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({ const moveClass = props.moveClass || `${props.name || 'v'}-move` - const firstChild = findFirstChild(prevChildren) + const firstChild = getFirstConnectedChild(prevChildren) if ( !firstChild || !hasCSSTransform( @@ -107,7 +110,10 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({ forceReflow() movedChildren.forEach(c => - handleMovedChildren(getEl(c) as ElementWithTransition, moveClass), + handleMovedChildren( + getTransitionElement(c) as ElementWithTransition, + moveClass, + ), ) }) @@ -180,12 +186,12 @@ function isValidTransitionBlock(block: Block): boolean { return !!(block instanceof Element || (isFragment(block) && block.insert)) } -function getEl(c: TransitionBlock): Element { - return (isFragment(c) ? c.nodes : c) as Element +function getTransitionElement(c: TransitionBlock): Element { + return (isFragment(c) ? (c.nodes as Element[])[0] : c) as Element } function recordPosition(c: TransitionBlock) { - newPositionMap.set(c, getEl(c).getBoundingClientRect()) + newPositionMap.set(c, getTransitionElement(c).getBoundingClientRect()) } function applyTranslation(c: TransitionBlock): TransitionBlock | undefined { @@ -193,17 +199,19 @@ function applyTranslation(c: TransitionBlock): TransitionBlock | undefined { baseApplyTranslation( positionMap.get(c)!, newPositionMap.get(c)!, - getEl(c) as ElementWithTransition, + getTransitionElement(c) as ElementWithTransition, ) ) { return c } } -function findFirstChild(children: TransitionBlock[]): Element | undefined { +function getFirstConnectedChild( + children: TransitionBlock[], +): Element | undefined { for (let i = 0; i < children.length; i++) { const child = children[i] - const el = getEl(child) + const el = getTransitionElement(child) if (el.isConnected) return el } } diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index b9bce9d6f8a..78d92affc58 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -229,7 +229,8 @@ function createVDOMComponent( parentInstance as any, ) } - frag.nodes = vnode.el as Node + + frag.nodes = [vnode.el as Node] simpleSetCurrentInstance(prev) } From 957aa098bbf1991cb7afe9d38bd2b92ab9a08b5c Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 12 Mar 2025 11:09:46 +0800 Subject: [PATCH 41/62] chore: update --- packages/runtime-vapor/src/components/TransitionGroup.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/runtime-vapor/src/components/TransitionGroup.ts b/packages/runtime-vapor/src/components/TransitionGroup.ts index 93bd202ce2c..13fac6ded87 100644 --- a/packages/runtime-vapor/src/components/TransitionGroup.ts +++ b/packages/runtime-vapor/src/components/TransitionGroup.ts @@ -142,11 +142,11 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({ const tag = props.tag if (tag) { - const el = document.createElement(tag) - insert(slottedBlock, el) + const container = document.createElement(tag) + insert(slottedBlock, container) // fallthrough attrs - renderEffect(() => setDynamicProps(el, [instance!.attrs])) - return [el] + renderEffect(() => setDynamicProps(container, [instance!.attrs])) + return container } else { const frag = __DEV__ ? new DynamicFragment('transitionGroup') From 4ad3ee90e2d8e8e7fec090fe7c74708d1225b629 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 12 Mar 2025 11:47:20 +0800 Subject: [PATCH 42/62] chore: rename --- packages/runtime-vapor/src/block.ts | 4 ++-- packages/runtime-vapor/src/components/TransitionGroup.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 65f7c4f35d5..1c6a1e72654 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -28,7 +28,7 @@ export interface TransitionOptions { export interface VaporTransitionHooks extends TransitionHooks { state: TransitionState props: TransitionProps - disabledOnMoving?: boolean + disabled?: boolean } export type TransitionBlock = @@ -161,7 +161,7 @@ export function insert( if ( block instanceof Element && (block as TransitionBlock).$transition && - !(block as TransitionBlock).$transition!.disabledOnMoving + !(block as TransitionBlock).$transition!.disabled ) { performTransitionEnter( block, diff --git a/packages/runtime-vapor/src/components/TransitionGroup.ts b/packages/runtime-vapor/src/components/TransitionGroup.ts index 13fac6ded87..19e2f6d8d84 100644 --- a/packages/runtime-vapor/src/components/TransitionGroup.ts +++ b/packages/runtime-vapor/src/components/TransitionGroup.ts @@ -67,10 +67,10 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({ if (isValidTransitionBlock(child)) { prevChildren.push(child) const hook = (child as TransitionBlock).$transition! - // disabled transition during moving, so the children will be + // disabled transition during enter, so the children will be // inserted into the correct position immediately. this prevents // `recordPosition` from getting incorrect positions in `onUpdated` - hook.disabledOnMoving = true + hook.disabled = true positionMap.set( child, getTransitionElement(child).getBoundingClientRect(), @@ -101,7 +101,7 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({ prevChildren.forEach(callPendingCbs) prevChildren.forEach(child => { - delete child.$transition!.disabledOnMoving + delete child.$transition!.disabled recordPosition(child) }) const movedChildren = prevChildren.filter(applyTranslation) From e7300a0bbb3d080941a8bbf381d81f37853f2cdf Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 12 Mar 2025 11:47:20 +0800 Subject: [PATCH 43/62] chore: update --- .../src/components/BaseTransition.ts | 19 +++++++------- packages/runtime-core/src/index.ts | 1 + packages/runtime-vapor/src/block.ts | 2 ++ .../src/components/Transition.ts | 25 +++++++------------ .../src/components/TransitionGroup.ts | 7 +++--- 5 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index fde42148304..5c51fde1f49 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -168,15 +168,7 @@ const BaseTransitionImpl: ComponentOptions = { const rawProps = toRaw(props) const { mode } = rawProps // check mode - if ( - __DEV__ && - mode && - mode !== 'in-out' && - mode !== 'out-in' && - mode !== 'default' - ) { - warn(`invalid mode: ${mode}`) - } + __DEV__ && checkTransitionMode(mode) if (state.isLeaving) { return emptyPlaceholder(child) @@ -622,3 +614,12 @@ export function getTransitionRawChildren( } return ret } + +/** + * dev-only + */ +export function checkTransitionMode(mode: string | undefined): void { + if (mode && mode !== 'in-out' && mode !== 'out-in' && mode !== 'default') { + warn(`invalid mode: ${mode}`) + } +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 596c31e5883..4eddabcff27 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -118,6 +118,7 @@ export { KeepAlive, type KeepAliveProps } from './components/KeepAlive' export { BaseTransition, BaseTransitionPropsValidators, + checkTransitionMode, type BaseTransitionProps, } from './components/BaseTransition' // For using custom directives diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 1c6a1e72654..cfe9d5abf52 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -28,6 +28,8 @@ export interface TransitionOptions { export interface VaporTransitionHooks extends TransitionHooks { state: TransitionState props: TransitionProps + // mark transition hooks as disabled so that it skips during + // inserting disabled?: boolean } diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index fbba29f3ba9..384bac42273 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -8,6 +8,7 @@ import { TransitionPropsValidators, type TransitionState, baseResolveTransitionHooks, + checkTransitionMode, currentInstance, leaveCbKey, resolveTransitionProps, @@ -36,15 +37,7 @@ export const VaporTransition: FunctionalComponent = if (!children) return const { mode } = props - if ( - __DEV__ && - mode && - mode !== 'in-out' && - mode !== 'out-in' && - mode !== 'default' - ) { - warn(`invalid mode: ${mode}`) - } + __DEV__ && checkTransitionMode(mode) applyTransitionEnterHooks(children, { state: useTransitionState(), @@ -119,13 +112,6 @@ export function resolveTransitionHooks( return hooks } -export function setTransitionHooks( - block: TransitionBlock, - hooks: VaporTransitionHooks, -): void { - block.$transition = hooks -} - export function applyTransitionEnterHooks( block: Block, hooks: VaporTransitionHooks, @@ -268,3 +254,10 @@ export function setTransitionHooksToFragment( } } } + +export function setTransitionHooks( + block: TransitionBlock, + hooks: VaporTransitionHooks, +): void { + block.$transition = hooks +} diff --git a/packages/runtime-vapor/src/components/TransitionGroup.ts b/packages/runtime-vapor/src/components/TransitionGroup.ts index 19e2f6d8d84..a9c5c0dac26 100644 --- a/packages/runtime-vapor/src/components/TransitionGroup.ts +++ b/packages/runtime-vapor/src/components/TransitionGroup.ts @@ -66,11 +66,10 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({ const child = children[i] if (isValidTransitionBlock(child)) { prevChildren.push(child) - const hook = (child as TransitionBlock).$transition! // disabled transition during enter, so the children will be // inserted into the correct position immediately. this prevents // `recordPosition` from getting incorrect positions in `onUpdated` - hook.disabled = true + child.$transition!.disabled = true positionMap.set( child, getTransitionElement(child).getBoundingClientRect(), @@ -129,12 +128,12 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({ for (let i = 0; i < children.length; i++) { const child = children[i] if (isValidTransitionBlock(child)) { - if ((child as TransitionBlock).$key != null) { + if (child.$key != null) { setTransitionHooks( child, resolveTransitionHooks(child, cssTransitionProps, state, instance!), ) - } else if (__DEV__ && (child as TransitionBlock).$key == null) { + } else if (__DEV__ && child.$key == null) { warn(` children must be keyed`) } } From af2eb2dbf5c415c468a5fb28d64043dca70aeac1 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 12 Mar 2025 22:36:38 +0800 Subject: [PATCH 44/62] wip: port tests and fix bugs --- .../__tests__/transition.spec.ts | 229 ++++++++++++++++++ .../vapor-e2e-test/transition/App.vue | 43 ++++ .../src/components/BaseTransition.ts | 10 +- .../src/components/Transition.ts | 18 +- 4 files changed, 294 insertions(+), 6 deletions(-) diff --git a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts index ebc9567b0c1..509751fad88 100644 --- a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts @@ -16,6 +16,7 @@ const { html, transitionStart, waitForElement, + click, } = setupPuppeteer() const duration = process.env.CI ? 200 : 50 @@ -42,6 +43,234 @@ describe('vapor transition', () => { await page().waitForSelector('#app') }) + describe('transition with v-if', () => { + test( + 'basic transition', + async () => { + const btnSelector = '.if-basic > button' + const containerSelector = '.if-basic > div' + const childSelector = `${containerSelector} > div` + + expect(await html(containerSelector)).toBe( + `
content
`, + ) + + // leave + expect( + (await transitionStart(btnSelector, childSelector)).classNames, + ).toStrictEqual(['test', 'v-leave-from', 'v-leave-active']) + + await nextFrame() + expect(await classList(childSelector)).toStrictEqual([ + 'test', + 'v-leave-active', + 'v-leave-to', + ]) + await transitionFinish() + expect(await html(containerSelector)).toBe('') + + // enter + expect( + (await transitionStart(btnSelector, childSelector)).classNames, + ).toStrictEqual(['test', 'v-enter-from', 'v-enter-active']) + await nextFrame() + expect(await classList(childSelector)).toStrictEqual([ + 'test', + 'v-enter-active', + 'v-enter-to', + ]) + await transitionFinish() + expect(await html(containerSelector)).toBe( + '
content
', + ) + }, + E2E_TIMEOUT, + ) + + test( + 'named transition', + async () => { + const btnSelector = '.if-named > button' + const containerSelector = '.if-named > div' + const childSelector = `${containerSelector} > div` + + expect(await html(containerSelector)).toBe( + '
content
', + ) + + // leave + expect( + (await transitionStart(btnSelector, childSelector)).classNames, + ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active']) + await nextFrame() + expect(await classList(childSelector)).toStrictEqual([ + 'test', + 'test-leave-active', + 'test-leave-to', + ]) + + await transitionFinish() + expect(await html(containerSelector)).toBe('') + + // enter + expect( + (await transitionStart(btnSelector, childSelector)).classNames, + ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active']) + await nextFrame() + expect(await classList(childSelector)).toStrictEqual([ + 'test', + 'test-enter-active', + 'test-enter-to', + ]) + await transitionFinish() + expect(await html(containerSelector)).toBe( + '
content
', + ) + }, + E2E_TIMEOUT, + ) + + test( + 'custom transition classes', + async () => { + const btnSelector = '.if-custom-classes > button' + const containerSelector = '.if-custom-classes > div' + const childSelector = `${containerSelector} > div` + + expect(await html(containerSelector)).toBe( + '
content
', + ) + // leave + expect( + (await transitionStart(btnSelector, childSelector)).classNames, + ).toStrictEqual(['test', 'bye-from', 'bye-active']) + await nextFrame() + expect(await classList(childSelector)).toStrictEqual([ + 'test', + 'bye-active', + 'bye-to', + ]) + await transitionFinish() + expect(await html(containerSelector)).toBe('') + + // enter + expect( + (await transitionStart(btnSelector, childSelector)).classNames, + ).toStrictEqual(['test', 'hello-from', 'hello-active']) + await nextFrame() + expect(await classList(childSelector)).toStrictEqual([ + 'test', + 'hello-active', + 'hello-to', + ]) + await transitionFinish() + expect(await html(containerSelector)).toBe( + '
content
', + ) + }, + E2E_TIMEOUT, + ) + + test( + 'transition with dynamic name', + async () => { + const btnSelector = '.if-dynamic-name > button.toggle' + const btnChangeNameSelector = '.if-dynamic-name > button.change' + const containerSelector = '.if-dynamic-name > div' + const childSelector = `${containerSelector} > div` + + expect(await html(containerSelector)).toBe( + '
content
', + ) + + // leave + expect( + (await transitionStart(btnSelector, childSelector)).classNames, + ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active']) + await nextFrame() + expect(await classList(childSelector)).toStrictEqual([ + 'test', + 'test-leave-active', + 'test-leave-to', + ]) + await transitionFinish() + expect(await html(containerSelector)).toBe('') + + // enter + await click(btnChangeNameSelector) + expect( + (await transitionStart(btnSelector, childSelector)).classNames, + ).toStrictEqual(['test', 'changed-enter-from', 'changed-enter-active']) + await nextFrame() + expect(await classList(childSelector)).toStrictEqual([ + 'test', + 'changed-enter-active', + 'changed-enter-to', + ]) + await transitionFinish() + expect(await html(containerSelector)).toBe( + '
content
', + ) + }, + E2E_TIMEOUT, + ) + test.todo('transition events without appear', async () => {}, E2E_TIMEOUT) + test.todo('events with arguments', async () => {}, E2E_TIMEOUT) + test.todo('onEnterCancelled', async () => {}, E2E_TIMEOUT) + test.todo('transition on appear', async () => {}, E2E_TIMEOUT) + test.todo('transition events with appear', async () => {}, E2E_TIMEOUT) + test.todo('no transition detected', async () => {}, E2E_TIMEOUT) + test.todo('animations', async () => {}, E2E_TIMEOUT) + test.todo('explicit transition type', async () => {}, E2E_TIMEOUT) + test.todo('transition on SVG elements', async () => {}, E2E_TIMEOUT) + test.todo( + 'custom transition higher-order component', + async () => {}, + E2E_TIMEOUT, + ) + test.todo( + 'transition on child components with empty root node', + async () => {}, + E2E_TIMEOUT, + ) + test.todo( + 'transition with v-if at component root-level', + async () => {}, + E2E_TIMEOUT, + ) + test.todo( + 'wrapping transition + fallthrough attrs', + async () => {}, + E2E_TIMEOUT, + ) + test.todo( + 'transition + fallthrough attrs (in-out mode)', + async () => {}, + E2E_TIMEOUT, + ) + }) + + describe('transition with v-show', () => { + test.todo('named transition with v-show', async () => {}, E2E_TIMEOUT) + test.todo('transition events with v-show', async () => {}, E2E_TIMEOUT) + test.todo('onLeaveCancelled (v-show only)', async () => {}, E2E_TIMEOUT) + test.todo('transition on appear with v-show', async () => {}, E2E_TIMEOUT) + test.todo( + 'transition events should not call onEnter with v-show false', + async () => {}, + E2E_TIMEOUT, + ) + test.todo('transition on appear with v-show', async () => {}, E2E_TIMEOUT) + }) + + describe('explicit durations', () => { + test.todo('single value', async () => {}, E2E_TIMEOUT) + test.todo('enter with explicit durations', async () => {}, E2E_TIMEOUT) + test.todo('leave with explicit durations', async () => {}, E2E_TIMEOUT) + test.todo('separate enter and leave', async () => {}, E2E_TIMEOUT) + test.todo('warn invalid durations', async () => {}, E2E_TIMEOUT) + }) + test( 'should work with v-show', async () => { diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue index b8470c10749..b5f2e77ab33 100644 --- a/packages-private/vapor-e2e-test/transition/App.vue +++ b/packages-private/vapor-e2e-test/transition/App.vue @@ -23,10 +23,53 @@ function toggleInteropComponent() { interopComponent.value = interopComponent.value === VaporCompA ? VDomComp : VaporCompA } + +const name = ref('test')