diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts index f1d509acf..d0a981c37 100644 --- a/packages/compiler-ssr/__tests__/ssrElement.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrElement.spec.ts @@ -1,5 +1,7 @@ import { getCompiledString } from './utils' import { compile } from '../src' +import { renderToString } from '@vue/server-renderer' +import { createApp } from '@vue/runtime-dom' describe('ssr: element', () => { test('basic elements', () => { @@ -71,6 +73,160 @@ describe('ssr: element', () => { `) }) + test('`, + ), + ).toMatchInlineSnapshot(` + "\`\`" + `) + + expect( + await renderToString( + createApp({ + data: () => ({ selected: 2 }), + template: `
`, + }), + ), + ).toMatchInlineSnapshot( + `"
"`, + ) + }) + + test('`, + ), + ).toMatchInlineSnapshot(` + "\`\`" + `) + + expect( + await renderToString( + createApp({ + template: `
`, + }), + ), + ).toMatchInlineSnapshot( + `"
"`, + ) + }) + + test('`) + .code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + let _temp0 + + _push(\`\`) + }" + `) + + expect( + await renderToString( + createApp({ + data: () => ({ obj: { value: 2 } }), + template: `
`, + }), + ), + ).toMatchInlineSnapshot( + `"
"`, + ) + }) + + test('`, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + let _temp0 + + _push(\`\`) + }" + `) + + expect( + await renderToString( + createApp({ + data: () => ({ obj: { value: 1 } }), + template: `
`, + }), + ), + ).toMatchInlineSnapshot( + `"
"`, + ) + }) + + test('`, + ).code, + ).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + let _temp0 + + _push(\`\`) + }" + `) + + expect( + await renderToString( + createApp({ + data: () => ({ obj: { value: 1 } }), + template: `
`, + }), + ), + ).toMatchInlineSnapshot( + `"
"`, + ) + }) + test('multiple _ssrInterpolate at parent and child import dependency once', () => { expect( compile(`
{{ hello }}
`).code, diff --git a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts index 4a12b0f7b..a515eec2c 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts @@ -57,6 +57,7 @@ import { type SSRTransformContext, processChildren, } from '../ssrCodegenTransform' +import { processSelectChildren } from '../utils' // for directives with children overwrite (e.g. v-html & v-text), we need to // store the raw children so that they can be added in the 2nd pass. @@ -139,6 +140,22 @@ export const ssrTransformElement: NodeTransform = (node, context) => { ]), ) } + } else if (node.tag === 'select') { + // // we need to determine the props to render for the dynamic v-model @@ -223,10 +240,17 @@ export const ssrTransformElement: NodeTransform = (node, context) => { context.onError( createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc), ) - } else if (isTextareaWithValue(node, prop) && prop.exp) { + } else if (isTagWithValueBind(node, 'textarea', prop) && prop.exp) { if (!needMergeProps) { node.children = [createInterpolation(prop.exp, prop.loc)] } + } else if (isTagWithValueBind(node, 'select', prop) && prop.exp) { + if (!needMergeProps) { + processSelectChildren(context, node.children, { + type: 'dynamicValue', + value: prop.exp, + }) + } } else if (!needMergeProps && prop.name !== 'on') { // Directive transforms. const directiveTransform = context.directiveTransforms[prop.name] @@ -326,6 +350,13 @@ export const ssrTransformElement: NodeTransform = (node, context) => { const name = prop.name if (node.tag === 'textarea' && name === 'value' && prop.value) { rawChildrenMap.set(node, escapeHtml(prop.value.content)) + } else if (node.tag === 'select' && name === 'value' && prop.value) { + if (!needMergeProps) { + processSelectChildren(context, node.children, { + type: 'staticValue', + value: prop.value.content, + }) + } } else if (!needMergeProps) { if (name === 'key' || name === 'ref') { continue @@ -399,12 +430,13 @@ function isTrueFalseValue(prop: DirectiveNode | AttributeNode) { } } -function isTextareaWithValue( +function isTagWithValueBind( node: PlainElementNode, + targetTag: string, prop: DirectiveNode, ): boolean { return !!( - node.tag === 'textarea' && + node.tag === targetTag && prop.name === 'bind' && isStaticArgOf(prop.arg, 'value') ) diff --git a/packages/compiler-ssr/src/transforms/ssrVModel.ts b/packages/compiler-ssr/src/transforms/ssrVModel.ts index cbe5b2b42..37a40919f 100644 --- a/packages/compiler-ssr/src/transforms/ssrVModel.ts +++ b/packages/compiler-ssr/src/transforms/ssrVModel.ts @@ -2,27 +2,23 @@ import { DOMErrorCodes, type DirectiveTransform, ElementTypes, - type ExpressionNode, NodeTypes, - type PlainElementNode, - type TemplateChildNode, createCallExpression, createConditionalExpression, createDOMCompilerError, createInterpolation, createObjectProperty, - createSimpleExpression, findProp, hasDynamicKeyVBind, transformModel, } from '@vue/compiler-dom' import { - SSR_INCLUDE_BOOLEAN_ATTR, SSR_LOOSE_CONTAIN, SSR_LOOSE_EQUAL, SSR_RENDER_DYNAMIC_MODEL, } from '../runtimeHelpers' import type { DirectiveTransformResult } from 'packages/compiler-core/src/transform' +import { findValueBinding, processSelectChildren } from '../utils' export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { const model = dir.exp! @@ -39,48 +35,6 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { } } - const processSelectChildren = (children: TemplateChildNode[]) => { - children.forEach(child => { - if (child.type === NodeTypes.ELEMENT) { - processOption(child as PlainElementNode) - } else if (child.type === NodeTypes.FOR) { - processSelectChildren(child.children) - } else if (child.type === NodeTypes.IF) { - child.branches.forEach(b => processSelectChildren(b.children)) - } - }) - } - - function processOption(plainNode: PlainElementNode) { - if (plainNode.tag === 'option') { - if (plainNode.props.findIndex(p => p.name === 'selected') === -1) { - const value = findValueBinding(plainNode) - plainNode.ssrCodegenNode!.elements.push( - createConditionalExpression( - createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [ - createConditionalExpression( - createCallExpression(`Array.isArray`, [model]), - createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [ - model, - value, - ]), - createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ - model, - value, - ]), - ), - ]), - createSimpleExpression(' selected', true), - createSimpleExpression('', true), - false /* no newline */, - ), - ) - } - } else if (plainNode.tag === 'optgroup') { - processSelectChildren(plainNode.children) - } - } - if (node.tagType === ElementTypes.ELEMENT) { const res: DirectiveTransformResult = { props: [] } const defaultProps = [ @@ -173,7 +127,10 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { checkDuplicatedValue() node.children = [createInterpolation(model, model.loc)] } else if (node.tag === 'select') { - processSelectChildren(node.children) + processSelectChildren(context, node.children, { + type: 'dynamicValue', + value: model, + }) } else { context.onError( createDOMCompilerError( @@ -189,12 +146,3 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { return transformModel(dir, node, context) } } - -function findValueBinding(node: PlainElementNode): ExpressionNode { - const valueBinding = findProp(node, 'value') - return valueBinding - ? valueBinding.type === NodeTypes.DIRECTIVE - ? valueBinding.exp! - : createSimpleExpression(valueBinding.value!.content, true) - : createSimpleExpression(`null`, false) -} diff --git a/packages/compiler-ssr/src/utils.ts b/packages/compiler-ssr/src/utils.ts new file mode 100644 index 000000000..7e1a98ede --- /dev/null +++ b/packages/compiler-ssr/src/utils.ts @@ -0,0 +1,115 @@ +import { + type ExpressionNode, + NodeTypes, + type PlainElementNode, + type TemplateChildNode, + type TransformContext, + createCallExpression, + createConditionalExpression, + createSimpleExpression, + findProp, +} from '@vue/compiler-core' +import { + SSR_INCLUDE_BOOLEAN_ATTR, + SSR_LOOSE_CONTAIN, + SSR_LOOSE_EQUAL, +} from './runtimeHelpers' + +export function findValueBinding(node: PlainElementNode): ExpressionNode { + const valueBinding = findProp(node, 'value') + return valueBinding + ? valueBinding.type === NodeTypes.DIRECTIVE + ? valueBinding.exp! + : createSimpleExpression(valueBinding.value!.content, true) + : createSimpleExpression(`null`, false) +} + +export type SelectValue = + | { + type: 'staticValue' + value: string + } + | { + type: 'dynamicValue' + value: ExpressionNode + } + | { + type: 'dynamicVBind' + tempId: string + } + +export const processSelectChildren = ( + context: TransformContext, + children: TemplateChildNode[], + selectValue: SelectValue, +): void => { + children.forEach(child => { + if (child.type === NodeTypes.ELEMENT) { + processOption(context, child as PlainElementNode, selectValue) + } else if (child.type === NodeTypes.FOR) { + processSelectChildren(context, child.children, selectValue) + } else if (child.type === NodeTypes.IF) { + child.branches.forEach(b => + processSelectChildren(context, b.children, selectValue), + ) + } + }) +} + +export function processOption( + context: TransformContext, + plainNode: PlainElementNode, + selectValue: SelectValue, +): void { + if (plainNode.tag === 'option') { + if (plainNode.props.findIndex(p => p.name === 'selected') === -1) { + const value = findValueBinding(plainNode) + + function createDynamicSelectExpression(selectValue: ExpressionNode) { + return createConditionalExpression( + createCallExpression(`Array.isArray`, [selectValue]), + createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [ + selectValue, + value, + ]), + createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ + selectValue, + value, + ]), + ) + } + + plainNode.ssrCodegenNode!.elements.push( + createConditionalExpression( + createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [ + selectValue.type === 'staticValue' + ? createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ + createSimpleExpression(selectValue.value, true), + value, + ]) + : selectValue.type === 'dynamicValue' + ? createDynamicSelectExpression(selectValue.value) + : createConditionalExpression( + createSimpleExpression( + `"value" in ${selectValue.tempId}`, + false, + ), + createDynamicSelectExpression( + createSimpleExpression( + `${selectValue.tempId}.value`, + false, + ), + ), + createSimpleExpression('false', false), + ), + ]), + createSimpleExpression(' selected', true), + createSimpleExpression('', true), + false /* no newline */, + ), + ) + } + } else if (plainNode.tag === 'optgroup') { + processSelectChildren(context, plainNode.children, selectValue) + } +}