diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
index d175d2dfc11..2fdd8010bc1 100644
--- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
@@ -275,14 +275,6 @@ describe('ssr: components', () => {
}"
`)
- expect(compile(``).code)
- .toMatchInlineSnapshot(`
- "
- return function ssrRender(_ctx, _push, _parent, _attrs) {
- _push(\`
\`)
- }"
- `)
-
expect(compile(``).code)
.toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
@@ -295,5 +287,93 @@ describe('ssr: components', () => {
}"
`)
})
+
+ // transition-group should flatten and concat its children fragments into
+ // a single one
+ describe('transition-group', () => {
+ test('basic', () => {
+ expect(
+ compile(
+ ``
+ ).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ _ssrRenderList(_ctx.list, (i) => {
+ _push(\`\`)
+ })
+ _push(\`\`)
+ }"
+ `)
+ })
+
+ test('with static tag', () => {
+ expect(
+ compile(
+ ``
+ ).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ _ssrRenderList(_ctx.list, (i) => {
+ _push(\`\`)
+ })
+ _push(\`
\`)
+ }"
+ `)
+ })
+
+ test('with dynamic tag', () => {
+ expect(
+ compile(
+ ``
+ ).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`<\${_ctx.someTag}>\`)
+ _ssrRenderList(_ctx.list, (i) => {
+ _push(\`\`)
+ })
+ _push(\`\${_ctx.someTag}>\`)
+ }"
+ `)
+ })
+
+ test('with multi fragments children', () => {
+ expect(
+ compile(
+ `
+
+
+ ok
+ `
+ ).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ _ssrRenderList(10, (i) => {
+ _push(\`\`)
+ })
+ _ssrRenderList(10, (i) => {
+ _push(\`\`)
+ })
+ if (_ctx.ok) {
+ _push(\`ok
\`)
+ } else {
+ _push(\`\`)
+ }
+ _push(\`\`)
+ }"
+ `)
+ })
+ })
})
})
diff --git a/packages/compiler-ssr/src/ssrCodegenTransform.ts b/packages/compiler-ssr/src/ssrCodegenTransform.ts
index 949db9c5813..cebce7433bf 100644
--- a/packages/compiler-ssr/src/ssrCodegenTransform.ts
+++ b/packages/compiler-ssr/src/ssrCodegenTransform.ts
@@ -128,7 +128,8 @@ function createChildContext(
export function processChildren(
children: TemplateChildNode[],
context: SSRTransformContext,
- asFragment = false
+ asFragment = false,
+ disableNestedFragments = false
) {
if (asFragment) {
context.pushStringPart(``)
@@ -176,10 +177,10 @@ export function processChildren(
)
break
case NodeTypes.IF:
- ssrProcessIf(child, context)
+ ssrProcessIf(child, context, disableNestedFragments)
break
case NodeTypes.FOR:
- ssrProcessFor(child, context)
+ ssrProcessFor(child, context, disableNestedFragments)
break
case NodeTypes.IF_BRANCH:
// no-op - handled by ssrProcessIf
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
index 69bf0662fe2..c2cf509c578 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
@@ -46,6 +46,7 @@ import {
ssrProcessSuspense,
ssrTransformSuspense
} from './ssrTransformSuspense'
+import { ssrProcessTransitionGroup } from './ssrTransformTransitionGroup'
import { isSymbol, isObject, isArray } from '@vue/shared'
// We need to construct the slot functions in the 1st pass to ensure proper
@@ -176,9 +177,11 @@ export function ssrProcessComponent(
return ssrProcessTeleport(node, context)
} else if (component === SUSPENSE) {
return ssrProcessSuspense(node, context)
+ } else if (component === TRANSITION_GROUP) {
+ return ssrProcessTransitionGroup(node, context)
} else {
// real fall-through (e.g. KeepAlive): just render its children.
- processChildren(node.children, context, component === TRANSITION_GROUP)
+ processChildren(node.children, context)
}
} else {
// finish up slot function expressions from the 1st pass.
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts
new file mode 100644
index 00000000000..3622900dd8a
--- /dev/null
+++ b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts
@@ -0,0 +1,41 @@
+import { ComponentNode, findProp, NodeTypes } from '@vue/compiler-dom'
+import { processChildren, SSRTransformContext } from '../ssrCodegenTransform'
+
+export function ssrProcessTransitionGroup(
+ node: ComponentNode,
+ context: SSRTransformContext
+) {
+ const tag = findProp(node, 'tag')
+ if (tag) {
+ if (tag.type === NodeTypes.DIRECTIVE) {
+ // dynamic :tag
+ context.pushStringPart(`<`)
+ context.pushStringPart(tag.exp!)
+ context.pushStringPart(`>`)
+
+ processChildren(
+ node.children,
+ context,
+ false,
+ /**
+ * TransitionGroup has the special runtime behavior of flattening and
+ * concatenating all children into a single fragment (in order for them to
+ * be pathced using the same key map) so we need to account for that here
+ * by disabling nested fragment wrappers from being generated.
+ */
+ true
+ )
+ context.pushStringPart(``)
+ context.pushStringPart(tag.exp!)
+ context.pushStringPart(`>`)
+ } else {
+ // static tag
+ context.pushStringPart(`<${tag.value!.content}>`)
+ processChildren(node.children, context, false, true)
+ context.pushStringPart(`${tag.value!.content}>`)
+ }
+ } else {
+ // fragment
+ processChildren(node.children, context, true, true)
+ }
+}
diff --git a/packages/compiler-ssr/src/transforms/ssrVFor.ts b/packages/compiler-ssr/src/transforms/ssrVFor.ts
index 0dd8b858eb2..583873b66ff 100644
--- a/packages/compiler-ssr/src/transforms/ssrVFor.ts
+++ b/packages/compiler-ssr/src/transforms/ssrVFor.ts
@@ -21,9 +21,14 @@ export const ssrTransformFor = createStructuralDirectiveTransform(
// This is called during the 2nd transform pass to construct the SSR-specific
// codegen nodes.
-export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
+export function ssrProcessFor(
+ node: ForNode,
+ context: SSRTransformContext,
+ disableNestedFragments = false
+) {
const needFragmentWrapper =
- node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
+ !disableNestedFragments &&
+ (node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT)
const renderLoop = createFunctionExpression(
createForLoopParams(node.parseResult)
)
@@ -32,13 +37,17 @@ export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
context,
needFragmentWrapper
)
- // v-for always renders a fragment
- context.pushStringPart(``)
+ // v-for always renders a fragment unless explicitly disabled
+ if (!disableNestedFragments) {
+ context.pushStringPart(``)
+ }
context.pushStatement(
createCallExpression(context.helper(SSR_RENDER_LIST), [
node.source,
renderLoop
])
)
- context.pushStringPart(``)
+ if (!disableNestedFragments) {
+ context.pushStringPart(``)
+ }
}
diff --git a/packages/compiler-ssr/src/transforms/ssrVIf.ts b/packages/compiler-ssr/src/transforms/ssrVIf.ts
index 9eea340b70e..57f77eafd30 100644
--- a/packages/compiler-ssr/src/transforms/ssrVIf.ts
+++ b/packages/compiler-ssr/src/transforms/ssrVIf.ts
@@ -22,18 +22,26 @@ export const ssrTransformIf = createStructuralDirectiveTransform(
// This is called during the 2nd transform pass to construct the SSR-specific
// codegen nodes.
-export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
+export function ssrProcessIf(
+ node: IfNode,
+ context: SSRTransformContext,
+ disableNestedFragments = false
+) {
const [rootBranch] = node.branches
const ifStatement = createIfStatement(
rootBranch.condition!,
- processIfBranch(rootBranch, context)
+ processIfBranch(rootBranch, context, disableNestedFragments)
)
context.pushStatement(ifStatement)
let currentIf = ifStatement
for (let i = 1; i < node.branches.length; i++) {
const branch = node.branches[i]
- const branchBlockStatement = processIfBranch(branch, context)
+ const branchBlockStatement = processIfBranch(
+ branch,
+ context,
+ disableNestedFragments
+ )
if (branch.condition) {
// else-if
currentIf = currentIf.alternate = createIfStatement(
@@ -55,10 +63,12 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
function processIfBranch(
branch: IfBranchNode,
- context: SSRTransformContext
+ context: SSRTransformContext,
+ disableNestedFragments = false
): BlockStatement {
const { children } = branch
const needFragmentWrapper =
+ !disableNestedFragments &&
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
// optimize away nested fragments when the only child is a ForNode
!(children.length === 1 && children[0].type === NodeTypes.FOR)
diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts
index fc0ebb9e42c..674eb795616 100644
--- a/packages/runtime-core/src/components/BaseTransition.ts
+++ b/packages/runtime-core/src/components/BaseTransition.ts
@@ -471,7 +471,7 @@ export function getTransitionRawChildren(
}
// #1126 if a transition children list contains multiple sub fragments, these
// fragments will be merged into a flat children array. Since each v-for
- // fragment may contain different static bindings inside, we need to de-top
+ // fragment may contain different static bindings inside, we need to de-op
// these children to force full diffs to ensure correct behavior.
if (keyedFragmentCount > 1) {
for (let i = 0; i < ret.length; i++) {