]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(transition/ssr): make transition appear work with SSR (#8859)
authoredison <daiwei521@126.com>
Tue, 24 Oct 2023 01:36:10 +0000 (09:36 +0800)
committerGitHub <noreply@github.com>
Tue, 24 Oct 2023 01:36:10 +0000 (09:36 +0800)
close #6951

packages/compiler-ssr/__tests__/ssrTransition.spec.ts [new file with mode: 0644]
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/compiler-ssr/src/transforms/ssrTransformTransition.ts [new file with mode: 0644]
packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/renderer.ts

diff --git a/packages/compiler-ssr/__tests__/ssrTransition.spec.ts b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts
new file mode 100644 (file)
index 0000000..319b390
--- /dev/null
@@ -0,0 +1,25 @@
+import { compile } from '../src'
+
+describe('transition', () => {
+  test('basic', () => {
+    expect(compile(`<transition><div>foo</div></transition>`).code)
+      .toMatchInlineSnapshot(`
+      "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        _push(\`<div\${_ssrRenderAttrs(_attrs)}>foo</div>\`)
+      }"
+    `)
+  })
+
+  test('with appear', () => {
+    expect(compile(`<transition appear><div>foo</div></transition>`).code)
+      .toMatchInlineSnapshot(`
+      "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        _push(\`<template><div\${_ssrRenderAttrs(_attrs)}>foo</div></template>\`)
+      }"
+    `)
+  })
+})
index dc8c6a4ae4f65ee8bad24f67dd84a636eb6737ad..93cae7db3c2628ae99e11d8a183246156580aee8 100644 (file)
@@ -56,6 +56,10 @@ import {
 } from './ssrTransformTransitionGroup'
 import { isSymbol, isObject, isArray } from '@vue/shared'
 import { buildSSRProps } from './ssrTransformElement'
+import {
+  ssrProcessTransition,
+  ssrTransformTransition
+} from './ssrTransformTransition'
 
 // We need to construct the slot functions in the 1st pass to ensure proper
 // scope tracking, but the children of each slot cannot be processed until
@@ -99,9 +103,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
   if (isSymbol(component)) {
     if (component === SUSPENSE) {
       return ssrTransformSuspense(node, context)
-    }
-    if (component === TRANSITION_GROUP) {
+    } else if (component === TRANSITION_GROUP) {
       return ssrTransformTransitionGroup(node, context)
+    } else if (component === TRANSITION) {
+      return ssrTransformTransition(node, context)
     }
     return // other built-in components: fallthrough
   }
@@ -216,9 +221,8 @@ export function ssrProcessComponent(
       if ((parent as WIPSlotEntry).type === WIP_SLOT) {
         context.pushStringPart(``)
       }
-      // #5351: filter out comment children inside transition
       if (component === TRANSITION) {
-        node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)
+        return ssrProcessTransition(node, context)
       }
       processChildren(node, context)
     }
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts
new file mode 100644 (file)
index 0000000..d09a806
--- /dev/null
@@ -0,0 +1,36 @@
+import {
+  ComponentNode,
+  findProp,
+  NodeTypes,
+  TransformContext
+} from '@vue/compiler-dom'
+import { processChildren, SSRTransformContext } from '../ssrCodegenTransform'
+
+const wipMap = new WeakMap<ComponentNode, Boolean>()
+
+export function ssrTransformTransition(
+  node: ComponentNode,
+  context: TransformContext
+) {
+  return () => {
+    const appear = findProp(node, 'appear', false, true)
+    wipMap.set(node, !!appear)
+  }
+}
+
+export function ssrProcessTransition(
+  node: ComponentNode,
+  context: SSRTransformContext
+) {
+  // #5351: filter out comment children inside transition
+  node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)
+
+  const appear = wipMap.get(node)
+  if (appear) {
+    context.pushStringPart(`<template>`)
+    processChildren(node, context, false, true)
+    context.pushStringPart(`</template>`)
+  } else {
+    processChildren(node, context, false, true)
+  }
+}
index f0a3a9333a7987eb538e4a051230405812acfac0..759804b97f1bc3337ee34fabfe2cd8fb4720ae7b 100644 (file)
@@ -18,10 +18,14 @@ import {
   createVNode,
   withDirectives,
   vModelCheckbox,
-  renderSlot
+  renderSlot,
+  Transition,
+  createCommentVNode,
+  vShow
 } from '@vue/runtime-dom'
 import { renderToString, SSRContext } from '@vue/server-renderer'
-import { PatchFlags } from '../../shared/src'
+import { PatchFlags } from '@vue/shared'
+import { vShowOldKey } from '../../runtime-dom/src/directives/vShow'
 
 function mountWithHydration(html: string, render: () => any) {
   const container = document.createElement('div')
@@ -1016,6 +1020,74 @@ describe('SSR hydration', () => {
     expect(`mismatch`).not.toHaveBeenWarned()
   })
 
+  test('transition appear', () => {
+    const { vnode, container } = mountWithHydration(
+      `<template><div>foo</div></template>`,
+      () =>
+        h(
+          Transition,
+          { appear: true },
+          {
+            default: () => h('div', 'foo')
+          }
+        )
+    )
+    expect(container.firstChild).toMatchInlineSnapshot(`
+      <div
+        class="v-enter-from v-enter-active"
+      >
+        foo
+      </div>
+    `)
+    expect(vnode.el).toBe(container.firstChild)
+    expect(`mismatch`).not.toHaveBeenWarned()
+  })
+
+  test('transition appear with v-if', () => {
+    const show = false
+    const { vnode, container } = mountWithHydration(
+      `<template><!----></template>`,
+      () =>
+        h(
+          Transition,
+          { appear: true },
+          {
+            default: () => (show ? h('div', 'foo') : createCommentVNode(''))
+          }
+        )
+    )
+    expect(container.firstChild).toMatchInlineSnapshot('<!---->')
+    expect(vnode.el).toBe(container.firstChild)
+    expect(`mismatch`).not.toHaveBeenWarned()
+  })
+
+  test('transition appear with v-show', () => {
+    const show = false
+    const { vnode, container } = mountWithHydration(
+      `<template><div style="display: none;">foo</div></template>`,
+      () =>
+        h(
+          Transition,
+          { appear: true },
+          {
+            default: () =>
+              withDirectives(createVNode('div', null, 'foo'), [[vShow, show]])
+          }
+        )
+    )
+    expect(container.firstChild).toMatchInlineSnapshot(`
+      <div
+        class="v-enter-from v-enter-active"
+        style="display: none;"
+      >
+        foo
+      </div>
+    `)
+    expect((container.firstChild as any)[vShowOldKey]).toBe('')
+    expect(vnode.el).toBe(container.firstChild)
+    expect(`mismatch`).not.toHaveBeenWarned()
+  })
+
   describe('mismatch handling', () => {
     test('text node', () => {
       const { container } = mountWithHydration(`foo`, () => 'bar')
index 097443dbc533b23f106af0d6a3dbc1341f2bf02e..4e91cb3d1cbc3f5ef89977ca718c1b4b3862cf77 100644 (file)
@@ -15,7 +15,7 @@ import { ComponentInternalInstance } from './component'
 import { invokeDirectiveHook } from './directives'
 import { warn } from './warning'
 import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
-import { RendererInternals } from './renderer'
+import { needTransition, RendererInternals } from './renderer'
 import { setRef } from './rendererTemplateRef'
 import {
   SuspenseImpl,
@@ -146,7 +146,17 @@ export function createHydrationFunctions(
         break
       case Comment:
         if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
-          nextNode = onMismatch()
+          if ((node as Element).tagName.toLowerCase() === 'template') {
+            const content = (vnode.el! as HTMLTemplateElement).content
+              .firstChild!
+
+            // replace <template> node with inner children
+            replaceNode(content, node, parentComponent)
+            vnode.el = node = content
+            nextNode = nextSibling(node)
+          } else {
+            nextNode = onMismatch()
+          }
         } else {
           nextNode = nextSibling(node)
         }
@@ -196,9 +206,10 @@ export function createHydrationFunctions(
       default:
         if (shapeFlag & ShapeFlags.ELEMENT) {
           if (
-            domType !== DOMNodeTypes.ELEMENT ||
-            (vnode.type as string).toLowerCase() !==
-              (node as Element).tagName.toLowerCase()
+            (domType !== DOMNodeTypes.ELEMENT ||
+              (vnode.type as string).toLowerCase() !==
+                (node as Element).tagName.toLowerCase()) &&
+            !isTemplateNode(node as Element)
           ) {
             nextNode = onMismatch()
           } else {
@@ -217,15 +228,6 @@ export function createHydrationFunctions(
           // on its sub-tree.
           vnode.slotScopeIds = slotScopeIds
           const container = parentNode(node)!
-          mountComponent(
-            vnode,
-            container,
-            null,
-            parentComponent,
-            parentSuspense,
-            isSVGContainer(container),
-            optimized
-          )
 
           // Locate the next node.
           if (isFragmentStart) {
@@ -241,6 +243,16 @@ export function createHydrationFunctions(
             nextNode = nextSibling(node)
           }
 
+          mountComponent(
+            vnode,
+            container,
+            null,
+            parentComponent,
+            parentSuspense,
+            isSVGContainer(container),
+            optimized
+          )
+
           // #3787
           // if component is async, it may get moved / unmounted before its
           // inner component is loaded, so we need to give it a placeholder
@@ -307,7 +319,7 @@ export function createHydrationFunctions(
     optimized: boolean
   ) => {
     optimized = optimized || !!vnode.dynamicChildren
-    const { type, props, patchFlag, shapeFlag, dirs } = vnode
+    const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode
     // #4006 for form elements with non-string v-model value bindings
     // e.g. <option :value="obj">, <input type="checkbox" :true-value="1">
     const forcePatchValue = (type === 'input' && dirs) || type === 'option'
@@ -359,12 +371,40 @@ export function createHydrationFunctions(
       if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
         invokeVNodeHook(vnodeHooks, parentComponent, vnode)
       }
+
+      // handle appear transition
+      let needCallTransitionHooks = false
+      if (isTemplateNode(el)) {
+        needCallTransitionHooks =
+          needTransition(parentSuspense, transition) &&
+          parentComponent &&
+          parentComponent.vnode.props &&
+          parentComponent.vnode.props.appear
+
+        const content = (el as HTMLTemplateElement).content
+          .firstChild as Element
+
+        if (needCallTransitionHooks) {
+          transition!.beforeEnter(content)
+        }
+
+        // replace <template> node with inner children
+        replaceNode(content, el, parentComponent)
+        vnode.el = el = content
+      }
+
       if (dirs) {
         invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
       }
-      if ((vnodeHooks = props && props.onVnodeMounted) || dirs) {
+
+      if (
+        (vnodeHooks = props && props.onVnodeMounted) ||
+        dirs ||
+        needCallTransitionHooks
+      ) {
         queueEffectWithSuspense(() => {
           vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
+          needCallTransitionHooks && transition!.enter(el)
           dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
         }, parentSuspense)
       }
@@ -582,5 +622,34 @@ export function createHydrationFunctions(
     return node
   }
 
+  const replaceNode = (
+    newNode: Node,
+    oldNode: Node,
+    parentComponent: ComponentInternalInstance | null
+  ): void => {
+    // replace node
+    const parentNode = oldNode.parentNode
+    if (parentNode) {
+      parentNode.replaceChild(newNode, oldNode)
+    }
+
+    // update vnode
+    let parent = parentComponent
+    while (parent) {
+      if (parent.vnode.el === oldNode) {
+        parent.vnode.el = newNode
+        parent.subTree.el = newNode
+      }
+      parent = parent.parent
+    }
+  }
+
+  const isTemplateNode = (node: Element): boolean => {
+    return (
+      node.nodeType === DOMNodeTypes.ELEMENT &&
+      node.tagName.toLowerCase() === 'template'
+    )
+  }
+
   return [hydrate, hydrateNode] as const
 }
index 383e17fb0f56251814ffb6ce3e21810f401f00b0..8799ecd473ca8ddc167970ed7acee7347c10ffce 100644 (file)
@@ -72,6 +72,7 @@ import { initFeatureFlags } from './featureFlags'
 import { isAsyncWrapper } from './apiAsyncComponent'
 import { isCompatEnabled } from './compat/compatConfig'
 import { DeprecationTypes } from './compat/compatConfig'
+import { TransitionHooks } from './components/BaseTransition'
 
 export interface Renderer<HostElement = RendererElement> {
   render: RootRenderFunction<HostElement>
@@ -701,10 +702,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
-    const needCallTransitionHooks =
-      (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
-      transition &&
-      !transition.persisted
+    const needCallTransitionHooks = needTransition(parentSuspense, transition)
     if (needCallTransitionHooks) {
       transition!.beforeEnter(el)
     }
@@ -2365,6 +2363,17 @@ function toggleRecurse(
   effect.allowRecurse = update.allowRecurse = allowed
 }
 
+export function needTransition(
+  parentSuspense: SuspenseBoundary | null,
+  transition: TransitionHooks | null
+) {
+  return (
+    (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
+    transition &&
+    !transition.persisted
+  )
+}
+
 /**
  * #1156
  * When a component is HMR-enabled, we need to make sure that all static nodes