Skip to content

Commit 67ad861

Browse files
committed
fix(compiler-ssr): add selected option attribute from select value
1 parent ba391f5 commit 67ad861

File tree

4 files changed

+311
-60
lines changed

4 files changed

+311
-60
lines changed

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

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { getCompiledString } from './utils'
22
import { compile } from '../src'
3+
import { renderToString } from '@vue/server-renderer'
4+
import { createApp } from '@vue/runtime-dom'
35

46
describe('ssr: element', () => {
57
test('basic elements', () => {
@@ -71,6 +73,160 @@ describe('ssr: element', () => {
7173
`)
7274
})
7375

76+
test('<select> with dynamic value assigns `selected` option attribute', async () => {
77+
expect(
78+
getCompiledString(
79+
`<select :value="selectValue"><option value="1"></option></select>`,
80+
),
81+
).toMatchInlineSnapshot(`
82+
"\`<select><option value="1"\${
83+
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.selectValue))
84+
? _ssrLooseContain(_ctx.selectValue, "1")
85+
: _ssrLooseEqual(_ctx.selectValue, "1"))) ? " selected" : ""
86+
}></option></select>\`"
87+
`)
88+
89+
expect(
90+
await renderToString(
91+
createApp({
92+
data: () => ({ selected: 2 }),
93+
template: `<div><select :value="selected"><option value="1">1</option><option value="2">2</option></select></div>`,
94+
}),
95+
),
96+
).toMatchInlineSnapshot(
97+
`"<div><select><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
98+
)
99+
})
100+
101+
test('<select> with static value assigns `selected` option attribute', async () => {
102+
expect(
103+
getCompiledString(
104+
`<select value="selectValue"><option value="1"></option></select>`,
105+
),
106+
).toMatchInlineSnapshot(`
107+
"\`<select><option value="1"\${
108+
(_ssrIncludeBooleanAttr(_ssrLooseEqual("selectValue", "1"))) ? " selected" : ""
109+
}></option></select>\`"
110+
`)
111+
112+
expect(
113+
await renderToString(
114+
createApp({
115+
template: `<div><select value="2"><option value="1">1</option><option value="2">2</option></select></div>`,
116+
}),
117+
),
118+
).toMatchInlineSnapshot(
119+
`"<div><select><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
120+
)
121+
})
122+
123+
test('<select> with dynamic v-bind assigns `selected` option attribute', async () => {
124+
expect(
125+
compile(`<select v-bind="obj"><option value="1"></option></select>`)
126+
.code,
127+
).toMatchInlineSnapshot(`
128+
"const { mergeProps: _mergeProps } = require("vue")
129+
const { ssrRenderAttrs: _ssrRenderAttrs, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual } = require("vue/server-renderer")
130+
131+
return function ssrRender(_ctx, _push, _parent, _attrs) {
132+
let _temp0
133+
134+
_push(\`<select\${
135+
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, _attrs))
136+
}><option value="1"\${
137+
(_ssrIncludeBooleanAttr(("value" in _temp0)
138+
? (Array.isArray(_temp0.value))
139+
? _ssrLooseContain(_temp0.value, "1")
140+
: _ssrLooseEqual(_temp0.value, "1")
141+
: false)) ? " selected" : ""
142+
}></option></select>\`)
143+
}"
144+
`)
145+
146+
expect(
147+
await renderToString(
148+
createApp({
149+
data: () => ({ obj: { value: 2 } }),
150+
template: `<div><select v-bind="obj"><option value="1">1</option><option value="2">2</option></select></div>`,
151+
}),
152+
),
153+
).toMatchInlineSnapshot(
154+
`"<div><select value="2"><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
155+
)
156+
})
157+
158+
test('<select> with dynamic v-bind and dynamic value bind assigns `selected` option attribute', async () => {
159+
expect(
160+
compile(
161+
`<select v-bind="obj" :value="selectValue"><option value="1"></option></select>`,
162+
).code,
163+
).toMatchInlineSnapshot(`
164+
"const { mergeProps: _mergeProps } = require("vue")
165+
const { ssrRenderAttrs: _ssrRenderAttrs, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual } = require("vue/server-renderer")
166+
167+
return function ssrRender(_ctx, _push, _parent, _attrs) {
168+
let _temp0
169+
170+
_push(\`<select\${
171+
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, { value: _ctx.selectValue }, _attrs))
172+
}><option value="1"\${
173+
(_ssrIncludeBooleanAttr(("value" in _temp0)
174+
? (Array.isArray(_temp0.value))
175+
? _ssrLooseContain(_temp0.value, "1")
176+
: _ssrLooseEqual(_temp0.value, "1")
177+
: false)) ? " selected" : ""
178+
}></option></select>\`)
179+
}"
180+
`)
181+
182+
expect(
183+
await renderToString(
184+
createApp({
185+
data: () => ({ obj: { value: 1 } }),
186+
template: `<div><select v-bind="obj" :value="2"><option value="1">1</option><option value="2">2</option></select></div>`,
187+
}),
188+
),
189+
).toMatchInlineSnapshot(
190+
`"<div><select value="2"><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
191+
)
192+
})
193+
194+
test('<select> with dynamic v-bind and static value bind assigns `selected` option attribute', async () => {
195+
expect(
196+
compile(
197+
`<select v-bind="obj" value="selectValue"><option value="1"></option></select>`,
198+
).code,
199+
).toMatchInlineSnapshot(`
200+
"const { mergeProps: _mergeProps } = require("vue")
201+
const { ssrRenderAttrs: _ssrRenderAttrs, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual } = require("vue/server-renderer")
202+
203+
return function ssrRender(_ctx, _push, _parent, _attrs) {
204+
let _temp0
205+
206+
_push(\`<select\${
207+
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, { value: "selectValue" }, _attrs))
208+
}><option value="1"\${
209+
(_ssrIncludeBooleanAttr(("value" in _temp0)
210+
? (Array.isArray(_temp0.value))
211+
? _ssrLooseContain(_temp0.value, "1")
212+
: _ssrLooseEqual(_temp0.value, "1")
213+
: false)) ? " selected" : ""
214+
}></option></select>\`)
215+
}"
216+
`)
217+
218+
expect(
219+
await renderToString(
220+
createApp({
221+
data: () => ({ obj: { value: 1 } }),
222+
template: `<div><select v-bind="obj" value="2"><option value="1">1</option><option value="2">2</option></select></div>`,
223+
}),
224+
),
225+
).toMatchInlineSnapshot(
226+
`"<div><select value="2"><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
227+
)
228+
})
229+
74230
test('multiple _ssrInterpolate at parent and child import dependency once', () => {
75231
expect(
76232
compile(`<div>{{ hello }}<textarea v-bind="a"></textarea></div>`).code,

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
type SSRTransformContext,
5858
processChildren,
5959
} from '../ssrCodegenTransform'
60+
import { processSelectChildren } from '../utils'
6061

6162
// for directives with children overwrite (e.g. v-html & v-text), we need to
6263
// store the raw children so that they can be added in the 2nd pass.
@@ -139,6 +140,22 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
139140
]),
140141
)
141142
}
143+
} else if (node.tag === 'select') {
144+
// <select> with dynamic v-bind. We don't know if the final props
145+
// will contain .value, so we will have to do something special:
146+
// assign the merged props to a temp variable, and check whether
147+
// it contains value (if yes, mark options selected).
148+
const tempId = `_temp${context.temps++}`
149+
propsExp.arguments = [
150+
createAssignmentExpression(
151+
createSimpleExpression(tempId, false),
152+
mergedProps,
153+
),
154+
]
155+
processSelectChildren(context, node.children, {
156+
type: 'dynamicVBind',
157+
tempId,
158+
})
142159
} else if (node.tag === 'input') {
143160
// <input v-bind="obj" v-model>
144161
// we need to determine the props to render for the dynamic v-model
@@ -223,10 +240,17 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
223240
context.onError(
224241
createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc),
225242
)
226-
} else if (isTextareaWithValue(node, prop) && prop.exp) {
243+
} else if (isTagWithValueBind(node, 'textarea', prop) && prop.exp) {
227244
if (!needMergeProps) {
228245
node.children = [createInterpolation(prop.exp, prop.loc)]
229246
}
247+
} else if (isTagWithValueBind(node, 'select', prop) && prop.exp) {
248+
if (!needMergeProps) {
249+
processSelectChildren(context, node.children, {
250+
type: 'dynamicValue',
251+
value: prop.exp,
252+
})
253+
}
230254
} else if (!needMergeProps && prop.name !== 'on') {
231255
// Directive transforms.
232256
const directiveTransform = context.directiveTransforms[prop.name]
@@ -326,6 +350,13 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
326350
const name = prop.name
327351
if (node.tag === 'textarea' && name === 'value' && prop.value) {
328352
rawChildrenMap.set(node, escapeHtml(prop.value.content))
353+
} else if (node.tag === 'select' && name === 'value' && prop.value) {
354+
if (!needMergeProps) {
355+
processSelectChildren(context, node.children, {
356+
type: 'staticValue',
357+
value: prop.value.content,
358+
})
359+
}
329360
} else if (!needMergeProps) {
330361
if (name === 'key' || name === 'ref') {
331362
continue
@@ -399,12 +430,13 @@ function isTrueFalseValue(prop: DirectiveNode | AttributeNode) {
399430
}
400431
}
401432

402-
function isTextareaWithValue(
433+
function isTagWithValueBind(
403434
node: PlainElementNode,
435+
targetTag: string,
404436
prop: DirectiveNode,
405437
): boolean {
406438
return !!(
407-
node.tag === 'textarea' &&
439+
node.tag === targetTag &&
408440
prop.name === 'bind' &&
409441
isStaticArgOf(prop.arg, 'value')
410442
)

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

Lines changed: 5 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,23 @@ import {
22
DOMErrorCodes,
33
type DirectiveTransform,
44
ElementTypes,
5-
type ExpressionNode,
65
NodeTypes,
7-
type PlainElementNode,
8-
type TemplateChildNode,
96
createCallExpression,
107
createConditionalExpression,
118
createDOMCompilerError,
129
createInterpolation,
1310
createObjectProperty,
14-
createSimpleExpression,
1511
findProp,
1612
hasDynamicKeyVBind,
1713
transformModel,
1814
} from '@vue/compiler-dom'
1915
import {
20-
SSR_INCLUDE_BOOLEAN_ATTR,
2116
SSR_LOOSE_CONTAIN,
2217
SSR_LOOSE_EQUAL,
2318
SSR_RENDER_DYNAMIC_MODEL,
2419
} from '../runtimeHelpers'
2520
import type { DirectiveTransformResult } from 'packages/compiler-core/src/transform'
21+
import { findValueBinding, processSelectChildren } from '../utils'
2622

2723
export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
2824
const model = dir.exp!
@@ -39,48 +35,6 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
3935
}
4036
}
4137

42-
const processSelectChildren = (children: TemplateChildNode[]) => {
43-
children.forEach(child => {
44-
if (child.type === NodeTypes.ELEMENT) {
45-
processOption(child as PlainElementNode)
46-
} else if (child.type === NodeTypes.FOR) {
47-
processSelectChildren(child.children)
48-
} else if (child.type === NodeTypes.IF) {
49-
child.branches.forEach(b => processSelectChildren(b.children))
50-
}
51-
})
52-
}
53-
54-
function processOption(plainNode: PlainElementNode) {
55-
if (plainNode.tag === 'option') {
56-
if (plainNode.props.findIndex(p => p.name === 'selected') === -1) {
57-
const value = findValueBinding(plainNode)
58-
plainNode.ssrCodegenNode!.elements.push(
59-
createConditionalExpression(
60-
createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [
61-
createConditionalExpression(
62-
createCallExpression(`Array.isArray`, [model]),
63-
createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [
64-
model,
65-
value,
66-
]),
67-
createCallExpression(context.helper(SSR_LOOSE_EQUAL), [
68-
model,
69-
value,
70-
]),
71-
),
72-
]),
73-
createSimpleExpression(' selected', true),
74-
createSimpleExpression('', true),
75-
false /* no newline */,
76-
),
77-
)
78-
}
79-
} else if (plainNode.tag === 'optgroup') {
80-
processSelectChildren(plainNode.children)
81-
}
82-
}
83-
8438
if (node.tagType === ElementTypes.ELEMENT) {
8539
const res: DirectiveTransformResult = { props: [] }
8640
const defaultProps = [
@@ -173,7 +127,10 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
173127
checkDuplicatedValue()
174128
node.children = [createInterpolation(model, model.loc)]
175129
} else if (node.tag === 'select') {
176-
processSelectChildren(node.children)
130+
processSelectChildren(context, node.children, {
131+
type: 'dynamicValue',
132+
value: model,
133+
})
177134
} else {
178135
context.onError(
179136
createDOMCompilerError(
@@ -189,12 +146,3 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
189146
return transformModel(dir, node, context)
190147
}
191148
}
192-
193-
function findValueBinding(node: PlainElementNode): ExpressionNode {
194-
const valueBinding = findProp(node, 'value')
195-
return valueBinding
196-
? valueBinding.type === NodeTypes.DIRECTIVE
197-
? valueBinding.exp!
198-
: createSimpleExpression(valueBinding.value!.content, true)
199-
: createSimpleExpression(`null`, false)
200-
}

0 commit comments

Comments
 (0)