mirror of https://github.com/vuejs/core.git
Merge abc4649c4f
into ba391f5fdf
This commit is contained in:
commit
bb852afc7d
|
@ -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('<select> with dynamic value assigns `selected` option attribute', async () => {
|
||||
expect(
|
||||
getCompiledString(
|
||||
`<select :value="selectValue"><option value="1"></option></select>`,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
"\`<select><option value="1"\${
|
||||
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.selectValue))
|
||||
? _ssrLooseContain(_ctx.selectValue, "1")
|
||||
: _ssrLooseEqual(_ctx.selectValue, "1"))) ? " selected" : ""
|
||||
}></option></select>\`"
|
||||
`)
|
||||
|
||||
expect(
|
||||
await renderToString(
|
||||
createApp({
|
||||
data: () => ({ selected: 2 }),
|
||||
template: `<div><select :value="selected"><option value="1">1</option><option value="2">2</option></select></div>`,
|
||||
}),
|
||||
),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<div><select><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
|
||||
)
|
||||
})
|
||||
|
||||
test('<select> with static value assigns `selected` option attribute', async () => {
|
||||
expect(
|
||||
getCompiledString(
|
||||
`<select value="selectValue"><option value="1"></option></select>`,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
"\`<select><option value="1"\${
|
||||
(_ssrIncludeBooleanAttr(_ssrLooseEqual("selectValue", "1"))) ? " selected" : ""
|
||||
}></option></select>\`"
|
||||
`)
|
||||
|
||||
expect(
|
||||
await renderToString(
|
||||
createApp({
|
||||
template: `<div><select value="2"><option value="1">1</option><option value="2">2</option></select></div>`,
|
||||
}),
|
||||
),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<div><select><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
|
||||
)
|
||||
})
|
||||
|
||||
test('<select> with dynamic v-bind assigns `selected` option attribute', async () => {
|
||||
expect(
|
||||
compile(`<select v-bind="obj"><option value="1"></option></select>`)
|
||||
.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(\`<select\${
|
||||
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, _attrs))
|
||||
}><option value="1"\${
|
||||
(_ssrIncludeBooleanAttr(("value" in _temp0)
|
||||
? (Array.isArray(_temp0.value))
|
||||
? _ssrLooseContain(_temp0.value, "1")
|
||||
: _ssrLooseEqual(_temp0.value, "1")
|
||||
: false)) ? " selected" : ""
|
||||
}></option></select>\`)
|
||||
}"
|
||||
`)
|
||||
|
||||
expect(
|
||||
await renderToString(
|
||||
createApp({
|
||||
data: () => ({ obj: { value: 2 } }),
|
||||
template: `<div><select v-bind="obj"><option value="1">1</option><option value="2">2</option></select></div>`,
|
||||
}),
|
||||
),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<div><select value="2"><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
|
||||
)
|
||||
})
|
||||
|
||||
test('<select> with dynamic v-bind and dynamic value bind assigns `selected` option attribute', async () => {
|
||||
expect(
|
||||
compile(
|
||||
`<select v-bind="obj" :value="selectValue"><option value="1"></option></select>`,
|
||||
).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(\`<select\${
|
||||
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, { value: _ctx.selectValue }, _attrs))
|
||||
}><option value="1"\${
|
||||
(_ssrIncludeBooleanAttr(("value" in _temp0)
|
||||
? (Array.isArray(_temp0.value))
|
||||
? _ssrLooseContain(_temp0.value, "1")
|
||||
: _ssrLooseEqual(_temp0.value, "1")
|
||||
: false)) ? " selected" : ""
|
||||
}></option></select>\`)
|
||||
}"
|
||||
`)
|
||||
|
||||
expect(
|
||||
await renderToString(
|
||||
createApp({
|
||||
data: () => ({ obj: { value: 1 } }),
|
||||
template: `<div><select v-bind="obj" :value="2"><option value="1">1</option><option value="2">2</option></select></div>`,
|
||||
}),
|
||||
),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<div><select value="2"><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
|
||||
)
|
||||
})
|
||||
|
||||
test('<select> with dynamic v-bind and static value bind assigns `selected` option attribute', async () => {
|
||||
expect(
|
||||
compile(
|
||||
`<select v-bind="obj" value="selectValue"><option value="1"></option></select>`,
|
||||
).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(\`<select\${
|
||||
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, { value: "selectValue" }, _attrs))
|
||||
}><option value="1"\${
|
||||
(_ssrIncludeBooleanAttr(("value" in _temp0)
|
||||
? (Array.isArray(_temp0.value))
|
||||
? _ssrLooseContain(_temp0.value, "1")
|
||||
: _ssrLooseEqual(_temp0.value, "1")
|
||||
: false)) ? " selected" : ""
|
||||
}></option></select>\`)
|
||||
}"
|
||||
`)
|
||||
|
||||
expect(
|
||||
await renderToString(
|
||||
createApp({
|
||||
data: () => ({ obj: { value: 1 } }),
|
||||
template: `<div><select v-bind="obj" value="2"><option value="1">1</option><option value="2">2</option></select></div>`,
|
||||
}),
|
||||
),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<div><select value="2"><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
|
||||
)
|
||||
})
|
||||
|
||||
test('multiple _ssrInterpolate at parent and child import dependency once', () => {
|
||||
expect(
|
||||
compile(`<div>{{ hello }}<textarea v-bind="a"></textarea></div>`).code,
|
||||
|
|
|
@ -98,6 +98,24 @@ describe('ssr: v-model', () => {
|
|||
}"
|
||||
`)
|
||||
|
||||
expect(
|
||||
compileWithWrapper(
|
||||
`<select v-model="model" value="2"><option value="1"></option></select>`,
|
||||
).code,
|
||||
).toMatchInlineSnapshot(`
|
||||
"const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
_push(\`<div\${
|
||||
_ssrRenderAttrs(_attrs)
|
||||
}><select><option value="1"\${
|
||||
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
|
||||
? _ssrLooseContain(_ctx.model, "1")
|
||||
: _ssrLooseEqual(_ctx.model, "1"))) ? " selected" : ""
|
||||
}></option></select></div>\`)
|
||||
}"
|
||||
`)
|
||||
|
||||
expect(
|
||||
compileWithWrapper(
|
||||
`<select multiple v-model="model"><option value="1" selected></option><option value="2"></option></select>`,
|
||||
|
|
|
@ -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.
|
||||
|
@ -81,6 +82,10 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
|
|||
const needTagForRuntime =
|
||||
node.tag === 'textarea' || node.tag.indexOf('-') > 0
|
||||
|
||||
const hasVModel = node.props.some(
|
||||
p => p.type === NodeTypes.DIRECTIVE && p.name === 'model',
|
||||
)
|
||||
|
||||
// v-bind="obj", v-bind:[key] and custom directives can potentially
|
||||
// overwrite other static attrs and can affect final rendering result,
|
||||
// so when they are present we need to bail out to full `renderAttrs`
|
||||
|
@ -139,6 +144,25 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
|
|||
]),
|
||||
)
|
||||
}
|
||||
} else if (node.tag === 'select') {
|
||||
// v-model takes priority over value
|
||||
if (!hasVModel) {
|
||||
// <select> with dynamic v-bind. We don't know if the final props
|
||||
// will contain .value, so we will have to do something special:
|
||||
// assign the merged props to a temp variable, and check whether
|
||||
// it contains value (if yes, mark options selected).
|
||||
const tempId = `_temp${context.temps++}`
|
||||
propsExp.arguments = [
|
||||
createAssignmentExpression(
|
||||
createSimpleExpression(tempId, false),
|
||||
mergedProps,
|
||||
),
|
||||
]
|
||||
processSelectChildren(context, node.children, {
|
||||
type: 'dynamicVBind',
|
||||
tempId,
|
||||
})
|
||||
}
|
||||
} else if (node.tag === 'input') {
|
||||
// <input v-bind="obj" v-model>
|
||||
// we need to determine the props to render for the dynamic v-model
|
||||
|
@ -223,10 +247,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 && !hasVModel) {
|
||||
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 +357,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 && !hasVModel) {
|
||||
processSelectChildren(context, node.children, {
|
||||
type: 'staticValue',
|
||||
value: prop.value.content,
|
||||
})
|
||||
}
|
||||
} else if (!needMergeProps) {
|
||||
if (name === 'key' || name === 'ref') {
|
||||
continue
|
||||
|
@ -399,12 +437,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')
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue