]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(hydration): hydrate VaporTransition (#14001)
authoredison <daiwei521@126.com>
Tue, 21 Oct 2025 00:47:14 +0000 (08:47 +0800)
committerGitHub <noreply@github.com>
Tue, 21 Oct 2025 00:47:14 +0000 (08:47 +0800)
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/index.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/components/Transition.ts
packages/runtime-vapor/src/directives/vShow.ts

index 38cce8b5a3575c76cf372e53a673ef692becd522..e1c10afdbe36cc204355950ab41003e500870537 100644 (file)
@@ -805,16 +805,16 @@ export function createHydrationFunctions(
     }
   }
 
-  const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
-    return (
-      node.nodeType === DOMNodeTypes.ELEMENT &&
-      (node as Element).tagName === 'TEMPLATE'
-    )
-  }
-
   return [hydrate, hydrateNode]
 }
 
+export const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
+  return (
+    node.nodeType === DOMNodeTypes.ELEMENT &&
+    (node as Element).tagName === 'TEMPLATE'
+  )
+}
+
 /**
  * Dev only
  */
index d0ae0fe79eebf9a0e487a82d6197cad47a544e1a..e4803b7cc3c56da26de89895aa19ca3dab9b28d6 100644 (file)
@@ -644,3 +644,7 @@ export {
  * @internal
  */
 export { createCanSetSetupRefChecker } from './rendererTemplateRef'
+/**
+ * @internal
+ */
+export { isTemplateNode } from './hydration'
index 213ec275c26ee657abb5fc80bb5731504f5b91b5..50e84e4c081b3e23cae63d5a616920893605b284 100644 (file)
@@ -2897,7 +2897,7 @@ describe('Vapor Mode hydration', () => {
     })
   })
 
-  describe.todo('transition', async () => {
+  describe('transition', async () => {
     test('transition appear', async () => {
       const { container } = await testHydration(
         `<template>
index c72bc0d517b5871e96ad3137a353bd92272e9cb8..a1f420deae58ed828c5f5e31188a544e337bb250 100644 (file)
@@ -1,4 +1,5 @@
 import {
+  type BaseTransitionProps,
   type GenericComponentInstance,
   type TransitionElement,
   type TransitionHooks,
@@ -9,7 +10,9 @@ import {
   baseResolveTransitionHooks,
   checkTransitionMode,
   currentInstance,
+  isTemplateNode,
   leaveCbKey,
+  queuePostFlushCb,
   resolveTransitionProps,
   useTransitionState,
   warn,
@@ -24,6 +27,11 @@ import {
 import { extend, isArray } from '@vue/shared'
 import { renderEffect } from '../renderEffect'
 import { isFragment } from '../fragment'
+import {
+  currentHydrationNode,
+  isHydrating,
+  setCurrentHydrationNode,
+} from '../dom/hydration'
 
 const decorate = (t: typeof VaporTransition) => {
   t.displayName = 'VaporTransition'
@@ -34,6 +42,33 @@ const decorate = (t: typeof VaporTransition) => {
 
 export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
   (props, { slots, attrs }) => {
+    // wrapped <transition appear>
+    let resetDisplay: Function | undefined
+    if (
+      isHydrating &&
+      currentHydrationNode &&
+      isTemplateNode(currentHydrationNode)
+    ) {
+      // replace <template> node with inner child
+      const {
+        content: { firstChild },
+        parentNode,
+      } = currentHydrationNode
+      if (firstChild) {
+        if (
+          firstChild instanceof HTMLElement ||
+          firstChild instanceof SVGElement
+        ) {
+          const originalDisplay = firstChild.style.display
+          firstChild.style.display = 'none'
+          resetDisplay = () => (firstChild.style.display = originalDisplay)
+        }
+
+        parentNode!.replaceChild(firstChild, currentHydrationNode)
+        setCurrentHydrationNode(firstChild)
+      }
+    }
+
     const children = (slots.default && slots.default()) as any as Block
     if (!children) return
 
@@ -41,7 +76,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
     const { mode } = props
     checkTransitionMode(mode)
 
-    let resolvedProps
+    let resolvedProps: BaseTransitionProps<Element>
     let isMounted = false
     renderEffect(() => {
       resolvedProps = resolveTransitionProps(props)
@@ -81,7 +116,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
       })
     }
 
-    applyTransitionHooks(
+    const hooks = applyTransitionHooks(
       children,
       {
         state: useTransitionState(),
@@ -91,6 +126,13 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
       fallthroughAttrs,
     )
 
+    if (resetDisplay && resolvedProps!.appear) {
+      const child = findTransitionBlock(children)!
+      hooks.beforeEnter(child)
+      resetDisplay()
+      queuePostFlushCb(() => hooks.enter(child))
+    }
+
     return children
   },
 )
index 079a6449e8e7664007ea523d9b4c6361c28982a1..57b21ba366338301ac217a0adbcefe489beef0dd 100644 (file)
@@ -61,44 +61,46 @@ function setDisplay(target: Block, value: unknown): void {
       el[vShowOriginalDisplay] =
         el.style.display === 'none' ? '' : el.style.display
     }
-    if (
-      (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
-      isHydrating
-    ) {
-      if (!value && el.style.display !== 'none') {
-        warnPropMismatch(
-          el,
-          'style',
-          MismatchTypes.STYLE,
-          `display: ${el.style.display}`,
-          'display: none',
-        )
-        logMismatchError()
 
-        el.style.display = 'none'
-        el[vShowOriginalDisplay] = ''
+    if ($transition) {
+      if (value) {
+        $transition.beforeEnter(target)
+        el.style.display = el[vShowOriginalDisplay]!
+        $transition.enter(target)
+      } else {
+        // during initial render, the element is not yet inserted into the
+        // DOM, and it is hidden, no need to trigger transition
+        if (target.isConnected) {
+          $transition.leave(target, () => {
+            el.style.display = 'none'
+          })
+        } else {
+          el.style.display = 'none'
+        }
       }
     } else {
-      if ($transition) {
-        if (value) {
-          $transition.beforeEnter(target)
-          el.style.display = el[vShowOriginalDisplay]!
-          $transition.enter(target)
-        } else {
-          // during initial render, the element is not yet inserted into the
-          // DOM, and it is hidden, no need to trigger transition
-          if (target.isConnected) {
-            $transition.leave(target, () => {
-              el.style.display = 'none'
-            })
-          } else {
-            el.style.display = 'none'
-          }
+      if (
+        (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+        isHydrating
+      ) {
+        if (!value && el.style.display !== 'none') {
+          warnPropMismatch(
+            el,
+            'style',
+            MismatchTypes.STYLE,
+            `display: ${el.style.display}`,
+            'display: none',
+          )
+          logMismatchError()
+
+          el.style.display = 'none'
+          el[vShowOriginalDisplay] = ''
         }
       } else {
         el.style.display = value ? el[vShowOriginalDisplay]! : 'none'
       }
     }
+
     el[vShowHidden] = !value
   } else if (__DEV__) {
     warn(