}"
`)
- expect(compile(`<transition-group><div/></transition-group>`).code)
- .toMatchInlineSnapshot(`
- "
- return function ssrRender(_ctx, _push, _parent, _attrs) {
- _push(\`<!--[--><div></div><!--]-->\`)
- }"
- `)
-
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
.toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent } = require(\\"vue\\")
}"
`)
})
+
+ // transition-group should flatten and concat its children fragments into
+ // a single one
+ describe('transition-group', () => {
+ test('basic', () => {
+ expect(
+ compile(
+ `<transition-group><div v-for="i in list"/></transition-group>`
+ ).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`<!--[-->\`)
+ _ssrRenderList(_ctx.list, (i) => {
+ _push(\`<div></div>\`)
+ })
+ _push(\`<!--]-->\`)
+ }"
+ `)
+ })
+
+ test('with static tag', () => {
+ expect(
+ compile(
+ `<transition-group tag="ul"><div v-for="i in list"/></transition-group>`
+ ).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`<ul>\`)
+ _ssrRenderList(_ctx.list, (i) => {
+ _push(\`<div></div>\`)
+ })
+ _push(\`</ul>\`)
+ }"
+ `)
+ })
+
+ test('with dynamic tag', () => {
+ expect(
+ compile(
+ `<transition-group :tag="someTag"><div v-for="i in list"/></transition-group>`
+ ).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`<\${_ctx.someTag}>\`)
+ _ssrRenderList(_ctx.list, (i) => {
+ _push(\`<div></div>\`)
+ })
+ _push(\`</\${_ctx.someTag}>\`)
+ }"
+ `)
+ })
+
+ test('with multi fragments children', () => {
+ expect(
+ compile(
+ `<transition-group>
+ <div v-for="i in 10"/>
+ <div v-for="i in 10"/>
+ <template v-if="ok"><div>ok</div></template>
+ </transition-group>`
+ ).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`<!--[-->\`)
+ _ssrRenderList(10, (i) => {
+ _push(\`<div></div>\`)
+ })
+ _ssrRenderList(10, (i) => {
+ _push(\`<div></div>\`)
+ })
+ if (_ctx.ok) {
+ _push(\`<div>ok</div>\`)
+ } else {
+ _push(\`<!---->\`)
+ }
+ _push(\`<!--]-->\`)
+ }"
+ `)
+ })
+ })
})
})
export function processChildren(
children: TemplateChildNode[],
context: SSRTransformContext,
- asFragment = false
+ asFragment = false,
+ disableNestedFragments = false
) {
if (asFragment) {
context.pushStringPart(`<!--[-->`)
)
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
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
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.
--- /dev/null
+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)
+ }
+}
// 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)
)
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(`<!--]-->`)
+ }
}
// 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(
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)
}
// #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++) {