]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(vapor): vapor transition
authordaiwei <daiwei521@126.com>
Thu, 27 Feb 2025 08:41:33 +0000 (16:41 +0800)
committerdaiwei <daiwei521@126.com>
Thu, 27 Feb 2025 09:36:21 +0000 (17:36 +0800)
12 files changed:
packages/compiler-vapor/src/generators/component.ts
packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-dom/src/components/Transition.ts
packages/runtime-dom/src/index.ts
packages/runtime-vapor/src/apiCreateApp.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/components/Transition.ts [new file with mode: 0644]
packages/runtime-vapor/src/directives/vShow.ts
packages/runtime-vapor/src/vdomInterop.ts

index 73e23150fa1d432fb317cb66225052c2ffec7afd..b131ad2e9470a3f2e9390356589b555c76526957 100644 (file)
@@ -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<CodeFragment[]>(
-    (acc, { name, value }) => {
+    (acc, { name, value }: InlineHandler) => {
       const handler = genEventHandler(context, value, undefined, false)
       return [...acc, `const ${name} = `, ...handler, NEWLINE]
     },
     [],
   )
-
   return [
     NEWLINE,
     ...inlineHandlers,
index 2b58bc3fc43dbe201624271f1312b51d45623a4d..ae89f36356bf6a19c5d6fe74b2207072a4f48a9c 100644 (file)
@@ -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<any>,
   state: TransitionState,
-  instance: ComponentInternalInstance,
+  instance: GenericComponentInstance,
   postClone?: (hooks: TransitionHooks) => void,
 ): TransitionHooks {
   const {
index c7150e38e808c8cbf4ee1df47d070984d2bfe8bb..51f42562eebfed4182aac9860d90d10f412e84ff 100644 (file)
@@ -557,3 +557,7 @@ export { startMeasure, endMeasure } from './profiling'
  * @internal
  */
 export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { applyTransitionEnter, applyTransitionLeave } from './renderer'
index fcbfdd0426cd28edbde6fa3b1db823a058248e55..fc3664de8c7812d4598008e8b2f7bbc368756f4f 100644 (file)
@@ -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,
index 6c6344bfcacbc8dc84961131dbca8d49f813634f..90cdaba4e73ab4935717c679dc8b302f0a41e881 100644 (file)
@@ -32,6 +32,20 @@ export interface TransitionProps extends BaseTransitionProps<Element> {
   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<TransitionProps> =
-  /*@__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
index 51c72fe2ed16f5f78dfedcb371f673e5acdb3d73..0cfd08e87a221f346241d75ff742b4fc2133b18f 100644 (file)
@@ -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'
index 8088e1aee6d49f4cae7989723575242e9027867d..da09a79a12a737f99c8bb7edacfec672756b08c3 100644 (file)
@@ -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<ParentNode, VaporComponent>
 
 const mountApp: AppMountFn<ParentNode> = (app, container) => {
   optimizePropertyLookup()
+  ensureVaporTransition()
 
   // clear content before mounting
   if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
index af18870559414e921b4aacc304a495c317443512..a4cfcea1f9c2e3c082cc8d2b260837f035657a76 100644 (file)
@@ -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) {
index 3c39612bb89d77ae185f6a4b278adec5377a5122..2c448bda2467a97937291b0fd531c4eefca175f0 100644 (file)
@@ -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 (file)
index 0000000..2eb87eb
--- /dev/null
@@ -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
+  }
+}
index ac4c066b71d6cbc78ed3d36fe1daf7bf0c2ba4b7..492d5225ef25306b3f5590886b0a6ce6a2313889 100644 (file)
@@ -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(
index 77228fd72a02fe85a5496daf7d89bc37e197a4d2..efe8223c6049f5133d1b4c7d756274a6a7637d11 100644 (file)
@@ -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']
 }