]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(ssr): ensure empty slots render as a comment node in Transition (#13396)
authoredison <daiwei521@126.com>
Wed, 23 Jul 2025 00:42:34 +0000 (08:42 +0800)
committerGitHub <noreply@github.com>
Wed, 23 Jul 2025 00:42:34 +0000 (08:42 +0800)
close #13394

packages/runtime-core/__tests__/hydration.spec.ts
packages/server-renderer/__tests__/ssrSlot.spec.ts
packages/server-renderer/src/helpers/ssrRenderSlot.ts

index 43af0583ce40098f99cf34f526ae8cb54c86ce88..6828e61ec56f1f063ff3d48b60d313f38ef11a6a 100644 (file)
@@ -1740,6 +1740,35 @@ describe('SSR hydration', () => {
     expect(`mismatch`).not.toHaveBeenWarned()
   })
 
+  // #13394
+  test('transition appear work with empty content', async () => {
+    const show = ref(true)
+    const { vnode, container } = mountWithHydration(
+      `<template><!----></template>`,
+      function (this: any) {
+        return h(
+          Transition,
+          { appear: true },
+          {
+            default: () =>
+              show.value
+                ? renderSlot(this.$slots, 'default')
+                : createTextVNode('foo'),
+          },
+        )
+      },
+    )
+
+    // empty slot render as a comment node
+    expect(container.firstChild!.nodeType).toBe(Node.COMMENT_NODE)
+    expect(vnode.el).toBe(container.firstChild)
+    expect(`mismatch`).not.toHaveBeenWarned()
+
+    show.value = false
+    await nextTick()
+    expect(container.innerHTML).toBe('foo')
+  })
+
   test('transition appear with v-if', () => {
     const show = false
     const { vnode, container } = mountWithHydration(
index 4cc7fd97ef2cd51609e85682a7a84535ee486d1a..214e6ee840b486411f6de050a06b1dcb48ff9770 100644 (file)
@@ -111,26 +111,106 @@ describe('ssr: slot', () => {
   })
 
   test('transition slot', async () => {
+    const ReusableTransition = {
+      template: `<transition><slot/></transition>`,
+    }
+
+    const ReusableTransitionWithAppear = {
+      template: `<transition appear><slot/></transition>`,
+    }
+
     expect(
       await renderToString(
         createApp({
           components: {
-            one: {
-              template: `<transition><slot/></transition>`,
-            },
+            one: ReusableTransition,
           },
           template: `<one><div v-if="false">foo</div></one>`,
         }),
       ),
     ).toBe(`<!---->`)
 
+    expect(await renderToString(createApp(ReusableTransition))).toBe(`<!---->`)
+
+    expect(await renderToString(createApp(ReusableTransitionWithAppear))).toBe(
+      `<template><!----></template>`,
+    )
+
     expect(
       await renderToString(
         createApp({
           components: {
-            one: {
-              template: `<transition><slot/></transition>`,
-            },
+            one: ReusableTransition,
+          },
+          template: `<one><slot/></one>`,
+        }),
+      ),
+    ).toBe(`<!---->`)
+
+    expect(
+      await renderToString(
+        createApp({
+          components: {
+            one: ReusableTransitionWithAppear,
+          },
+          template: `<one><slot/></one>`,
+        }),
+      ),
+    ).toBe(`<template><!----></template>`)
+
+    expect(
+      await renderToString(
+        createApp({
+          render() {
+            return h(ReusableTransition, null, {
+              default: () => null,
+            })
+          },
+        }),
+      ),
+    ).toBe(`<!---->`)
+
+    expect(
+      await renderToString(
+        createApp({
+          render() {
+            return h(ReusableTransitionWithAppear, null, {
+              default: () => null,
+            })
+          },
+        }),
+      ),
+    ).toBe(`<template><!----></template>`)
+
+    expect(
+      await renderToString(
+        createApp({
+          render() {
+            return h(ReusableTransitionWithAppear, null, {
+              default: () => [],
+            })
+          },
+        }),
+      ),
+    ).toBe(`<template><!----></template>`)
+
+    expect(
+      await renderToString(
+        createApp({
+          render() {
+            return h(ReusableTransition, null, {
+              default: () => [],
+            })
+          },
+        }),
+      ),
+    ).toBe(`<!---->`)
+
+    expect(
+      await renderToString(
+        createApp({
+          components: {
+            one: ReusableTransition,
           },
           template: `<one><div v-if="true">foo</div></one>`,
         }),
index 19aa4ce63b76c1a1118fd13a4dc32c6bae35533f..2f93a12de9de78042ff657b9509be565a4f8aa13 100644 (file)
@@ -74,6 +74,8 @@ export function ssrRenderSlotInner(
         )
       } else if (fallbackRenderFn) {
         fallbackRenderFn()
+      } else if (transition) {
+        push(`<!---->`)
       }
     } else {
       // ssr slot.
@@ -110,13 +112,19 @@ export function ssrRenderSlotInner(
           end--
         }
 
-        for (let i = start; i < end; i++) {
-          push(slotBuffer[i])
+        if (start < end) {
+          for (let i = start; i < end; i++) {
+            push(slotBuffer[i])
+          }
+        } else if (transition) {
+          push(`<!---->`)
         }
       }
     }
   } else if (fallbackRenderFn) {
     fallbackRenderFn()
+  } else if (transition) {
+    push(`<!---->`)
   }
 }