Skip to content

Commit a5d6f80

Browse files
committed
fix(compiler-ssr): generate correct children for transition-group
fix #2510
1 parent 55d99d7 commit a5d6f80

File tree

7 files changed

+166
-22
lines changed

7 files changed

+166
-22
lines changed

packages/compiler-ssr/__tests__/ssrComponent.spec.ts

+88-8
Original file line numberDiff line numberDiff line change
@@ -275,14 +275,6 @@ describe('ssr: components', () => {
275275
}"
276276
`)
277277

278-
expect(compile(`<transition-group><div/></transition-group>`).code)
279-
.toMatchInlineSnapshot(`
280-
"
281-
return function ssrRender(_ctx, _push, _parent, _attrs) {
282-
_push(\`<!--[--><div></div><!--]-->\`)
283-
}"
284-
`)
285-
286278
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
287279
.toMatchInlineSnapshot(`
288280
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
@@ -295,5 +287,93 @@ describe('ssr: components', () => {
295287
}"
296288
`)
297289
})
290+
291+
// transition-group should flatten and concat its children fragments into
292+
// a single one
293+
describe('transition-group', () => {
294+
test('basic', () => {
295+
expect(
296+
compile(
297+
`<transition-group><div v-for="i in list"/></transition-group>`
298+
).code
299+
).toMatchInlineSnapshot(`
300+
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
301+
302+
return function ssrRender(_ctx, _push, _parent, _attrs) {
303+
_push(\`<!--[-->\`)
304+
_ssrRenderList(_ctx.list, (i) => {
305+
_push(\`<div></div>\`)
306+
})
307+
_push(\`<!--]-->\`)
308+
}"
309+
`)
310+
})
311+
312+
test('with static tag', () => {
313+
expect(
314+
compile(
315+
`<transition-group tag="ul"><div v-for="i in list"/></transition-group>`
316+
).code
317+
).toMatchInlineSnapshot(`
318+
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
319+
320+
return function ssrRender(_ctx, _push, _parent, _attrs) {
321+
_push(\`<ul>\`)
322+
_ssrRenderList(_ctx.list, (i) => {
323+
_push(\`<div></div>\`)
324+
})
325+
_push(\`</ul>\`)
326+
}"
327+
`)
328+
})
329+
330+
test('with dynamic tag', () => {
331+
expect(
332+
compile(
333+
`<transition-group :tag="someTag"><div v-for="i in list"/></transition-group>`
334+
).code
335+
).toMatchInlineSnapshot(`
336+
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
337+
338+
return function ssrRender(_ctx, _push, _parent, _attrs) {
339+
_push(\`<\${_ctx.someTag}>\`)
340+
_ssrRenderList(_ctx.list, (i) => {
341+
_push(\`<div></div>\`)
342+
})
343+
_push(\`</\${_ctx.someTag}>\`)
344+
}"
345+
`)
346+
})
347+
348+
test('with multi fragments children', () => {
349+
expect(
350+
compile(
351+
`<transition-group>
352+
<div v-for="i in 10"/>
353+
<div v-for="i in 10"/>
354+
<template v-if="ok"><div>ok</div></template>
355+
</transition-group>`
356+
).code
357+
).toMatchInlineSnapshot(`
358+
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
359+
360+
return function ssrRender(_ctx, _push, _parent, _attrs) {
361+
_push(\`<!--[-->\`)
362+
_ssrRenderList(10, (i) => {
363+
_push(\`<div></div>\`)
364+
})
365+
_ssrRenderList(10, (i) => {
366+
_push(\`<div></div>\`)
367+
})
368+
if (_ctx.ok) {
369+
_push(\`<div>ok</div>\`)
370+
} else {
371+
_push(\`<!---->\`)
372+
}
373+
_push(\`<!--]-->\`)
374+
}"
375+
`)
376+
})
377+
})
298378
})
299379
})

packages/compiler-ssr/src/ssrCodegenTransform.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ function createChildContext(
128128
export function processChildren(
129129
children: TemplateChildNode[],
130130
context: SSRTransformContext,
131-
asFragment = false
131+
asFragment = false,
132+
disableNestedFragments = false
132133
) {
133134
if (asFragment) {
134135
context.pushStringPart(`<!--[-->`)
@@ -176,10 +177,10 @@ export function processChildren(
176177
)
177178
break
178179
case NodeTypes.IF:
179-
ssrProcessIf(child, context)
180+
ssrProcessIf(child, context, disableNestedFragments)
180181
break
181182
case NodeTypes.FOR:
182-
ssrProcessFor(child, context)
183+
ssrProcessFor(child, context, disableNestedFragments)
183184
break
184185
case NodeTypes.IF_BRANCH:
185186
// no-op - handled by ssrProcessIf

packages/compiler-ssr/src/transforms/ssrTransformComponent.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
ssrProcessSuspense,
4747
ssrTransformSuspense
4848
} from './ssrTransformSuspense'
49+
import { ssrProcessTransitionGroup } from './ssrTransformTransitionGroup'
4950
import { isSymbol, isObject, isArray } from '@vue/shared'
5051

5152
// We need to construct the slot functions in the 1st pass to ensure proper
@@ -176,9 +177,11 @@ export function ssrProcessComponent(
176177
return ssrProcessTeleport(node, context)
177178
} else if (component === SUSPENSE) {
178179
return ssrProcessSuspense(node, context)
180+
} else if (component === TRANSITION_GROUP) {
181+
return ssrProcessTransitionGroup(node, context)
179182
} else {
180183
// real fall-through (e.g. KeepAlive): just render its children.
181-
processChildren(node.children, context, component === TRANSITION_GROUP)
184+
processChildren(node.children, context)
182185
}
183186
} else {
184187
// finish up slot function expressions from the 1st pass.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ComponentNode, findProp, NodeTypes } from '@vue/compiler-dom'
2+
import { processChildren, SSRTransformContext } from '../ssrCodegenTransform'
3+
4+
export function ssrProcessTransitionGroup(
5+
node: ComponentNode,
6+
context: SSRTransformContext
7+
) {
8+
const tag = findProp(node, 'tag')
9+
if (tag) {
10+
if (tag.type === NodeTypes.DIRECTIVE) {
11+
// dynamic :tag
12+
context.pushStringPart(`<`)
13+
context.pushStringPart(tag.exp!)
14+
context.pushStringPart(`>`)
15+
16+
processChildren(
17+
node.children,
18+
context,
19+
false,
20+
/**
21+
* TransitionGroup has the special runtime behavior of flattening and
22+
* concatenating all children into a single fragment (in order for them to
23+
* be pathced using the same key map) so we need to account for that here
24+
* by disabling nested fragment wrappers from being generated.
25+
*/
26+
true
27+
)
28+
context.pushStringPart(`</`)
29+
context.pushStringPart(tag.exp!)
30+
context.pushStringPart(`>`)
31+
} else {
32+
// static tag
33+
context.pushStringPart(`<${tag.value!.content}>`)
34+
processChildren(node.children, context, false, true)
35+
context.pushStringPart(`</${tag.value!.content}>`)
36+
}
37+
} else {
38+
// fragment
39+
processChildren(node.children, context, true, true)
40+
}
41+
}

packages/compiler-ssr/src/transforms/ssrVFor.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@ export const ssrTransformFor = createStructuralDirectiveTransform(
2121

2222
// This is called during the 2nd transform pass to construct the SSR-specific
2323
// codegen nodes.
24-
export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
24+
export function ssrProcessFor(
25+
node: ForNode,
26+
context: SSRTransformContext,
27+
disableNestedFragments = false
28+
) {
2529
const needFragmentWrapper =
26-
node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
30+
!disableNestedFragments &&
31+
(node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT)
2732
const renderLoop = createFunctionExpression(
2833
createForLoopParams(node.parseResult)
2934
)
@@ -32,13 +37,17 @@ export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
3237
context,
3338
needFragmentWrapper
3439
)
35-
// v-for always renders a fragment
36-
context.pushStringPart(`<!--[-->`)
40+
// v-for always renders a fragment unless explicitly disabled
41+
if (!disableNestedFragments) {
42+
context.pushStringPart(`<!--[-->`)
43+
}
3744
context.pushStatement(
3845
createCallExpression(context.helper(SSR_RENDER_LIST), [
3946
node.source,
4047
renderLoop
4148
])
4249
)
43-
context.pushStringPart(`<!--]-->`)
50+
if (!disableNestedFragments) {
51+
context.pushStringPart(`<!--]-->`)
52+
}
4453
}

packages/compiler-ssr/src/transforms/ssrVIf.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,26 @@ export const ssrTransformIf = createStructuralDirectiveTransform(
2222

2323
// This is called during the 2nd transform pass to construct the SSR-specific
2424
// codegen nodes.
25-
export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
25+
export function ssrProcessIf(
26+
node: IfNode,
27+
context: SSRTransformContext,
28+
disableNestedFragments = false
29+
) {
2630
const [rootBranch] = node.branches
2731
const ifStatement = createIfStatement(
2832
rootBranch.condition!,
29-
processIfBranch(rootBranch, context)
33+
processIfBranch(rootBranch, context, disableNestedFragments)
3034
)
3135
context.pushStatement(ifStatement)
3236

3337
let currentIf = ifStatement
3438
for (let i = 1; i < node.branches.length; i++) {
3539
const branch = node.branches[i]
36-
const branchBlockStatement = processIfBranch(branch, context)
40+
const branchBlockStatement = processIfBranch(
41+
branch,
42+
context,
43+
disableNestedFragments
44+
)
3745
if (branch.condition) {
3846
// else-if
3947
currentIf = currentIf.alternate = createIfStatement(
@@ -55,10 +63,12 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
5563

5664
function processIfBranch(
5765
branch: IfBranchNode,
58-
context: SSRTransformContext
66+
context: SSRTransformContext,
67+
disableNestedFragments = false
5968
): BlockStatement {
6069
const { children } = branch
6170
const needFragmentWrapper =
71+
!disableNestedFragments &&
6272
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
6373
// optimize away nested fragments when the only child is a ForNode
6474
!(children.length === 1 && children[0].type === NodeTypes.FOR)

packages/runtime-core/src/components/BaseTransition.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ export function getTransitionRawChildren(
471471
}
472472
// #1126 if a transition children list contains multiple sub fragments, these
473473
// fragments will be merged into a flat children array. Since each v-for
474-
// fragment may contain different static bindings inside, we need to de-top
474+
// fragment may contain different static bindings inside, we need to de-op
475475
// these children to force full diffs to ensure correct behavior.
476476
if (keyedFragmentCount > 1) {
477477
for (let i = 0; i < ret.length; i++) {

0 commit comments

Comments
 (0)