From a5d6f8091e3761447b7fec0e3d1346eb83402a0a Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 27 Nov 2020 12:22:14 -0500 Subject: [PATCH] fix(compiler-ssr): generate correct children for transition-group fix #2510 --- .../__tests__/ssrComponent.spec.ts | 96 +++++++++++++++++-- .../compiler-ssr/src/ssrCodegenTransform.ts | 7 +- .../src/transforms/ssrTransformComponent.ts | 5 +- .../transforms/ssrTransformTransitionGroup.ts | 41 ++++++++ .../compiler-ssr/src/transforms/ssrVFor.ts | 19 +++- .../compiler-ssr/src/transforms/ssrVIf.ts | 18 +++- .../src/components/BaseTransition.ts | 2 +- 7 files changed, 166 insertions(+), 22 deletions(-) create mode 100644 packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts 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(\`\`) + }" + `) + }) + + test('with multi fragments children', () => { + expect( + compile( + ` +
+
+ + ` + ).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(``) + } else { + // static tag + context.pushStringPart(`<${tag.value!.content}>`) + processChildren(node.children, context, false, true) + context.pushStringPart(``) + } + } 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++) {