Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions packages/compiler-ssr/__tests__/ssrVModel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,197 @@ describe('ssr: v-model', () => {
_push(\`<!--]--></optgroup></select></div>\`)
}"
`)

expect(
compileWithWrapper(
`<select v-model="model"><option>foo</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\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, "foo")
: _ssrLooseEqual(_ctx.model, "foo"))) ? " selected" : ""
}>foo</option></select></div>\`)
}"
`)

expect(
compileWithWrapper(
`<select v-model="model"><option> foo </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\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, "foo")
: _ssrLooseEqual(_ctx.model, "foo"))) ? " selected" : ""
}> foo </option></select></div>\`)
}"
`)

expect(
compileWithWrapper(
`<select v-model="model"><option>{{ myValue }}</option></select>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><select><option\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, _ctx.myValue)
: _ssrLooseEqual(_ctx.model, _ctx.myValue))) ? " selected" : ""
}>\${
_ssrInterpolate(_ctx.myValue)
}</option></select></div>\`)
}"
`)

expect(
compileWithWrapper(
`<select v-model="model">
<option>
A
B
<!--comment-->
<!--comment-->
C
{{ myValue1 }}
D
<!--comment-->
{{ myValue2 }}
<!--comment-->E
<!--comment-->
</option>
</select>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><select><option\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, \`\${
"A B C "
}\${
_ctx.myValue1
}\${
" D "
}\${
_ctx.myValue2
}\${
" E"
}\`)
: _ssrLooseEqual(_ctx.model, \`\${
"A B C "
}\${
_ctx.myValue1
}\${
" D "
}\${
_ctx.myValue2
}\${
" E"
}\`))) ? " selected" : ""
}> A B <!--comment--><!--comment--> C \${
_ssrInterpolate(_ctx.myValue1)
} D <!--comment--> \${
_ssrInterpolate(_ctx.myValue2)
} <!--comment-->E <!--comment--></option></select></div>\`)
}"
`)

expect(
compileWithWrapper(
`<select v-model="model"><option v-text="foo"></option></select>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><select><option\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, _ctx.foo)
: _ssrLooseEqual(_ctx.model, _ctx.foo))) ? " selected" : ""
}>\${
_ssrInterpolate(_ctx.foo)
}</option></select></div>\`)
}"
`)

expect(
compileWithWrapper(
`<select v-model="model"><option v-text="'foo'"></option></select>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><select><option\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, 'foo')
: _ssrLooseEqual(_ctx.model, 'foo'))) ? " selected" : ""
}>\${
_ssrInterpolate('foo')
}</option></select></div>\`)
}"
`)

expect(
compileWithWrapper(`<select v-model="model"><option></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\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, "")
: _ssrLooseEqual(_ctx.model, ""))) ? " selected" : ""
}></option></select></div>\`)
}"
`)

expect(
compileWithWrapper(
`<select multiple v-model="model"><option>1</option><option>2</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 multiple><option\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, "1")
: _ssrLooseEqual(_ctx.model, "1"))) ? " selected" : ""
}>1</option><option\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, "2")
: _ssrLooseEqual(_ctx.model, "2"))) ? " selected" : ""
}>2</option></select></div>\`)
}"
`)
})

test('<input type="radio">', () => {
Expand Down
97 changes: 96 additions & 1 deletion packages/compiler-ssr/src/transforms/ssrVModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import {
NodeTypes,
type PlainElementNode,
type TemplateChildNode,
type TemplateLiteral,
type TextNode,
createCallExpression,
createConditionalExpression,
createDOMCompilerError,
createInterpolation,
createObjectProperty,
createSimpleExpression,
createTemplateLiteral,
findDir,
findProp,
hasDynamicKeyVBind,
transformModel,
Expand Down Expand Up @@ -54,7 +58,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
function processOption(plainNode: PlainElementNode) {
if (plainNode.tag === 'option') {
if (plainNode.props.findIndex(p => p.name === 'selected') === -1) {
const value = findValueBinding(plainNode)
const value = findOptionValue(plainNode)
plainNode.ssrCodegenNode!.elements.push(
createConditionalExpression(
createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [
Expand Down Expand Up @@ -190,6 +194,59 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
}
}

function findOptionValue(
node: PlainElementNode,
): ExpressionNode | TemplateLiteral {
const valueBinding = findProp(node, 'value')
if (valueBinding) {
return valueBinding.type === NodeTypes.DIRECTIVE
? valueBinding.exp!
: createSimpleExpression(valueBinding.value!.content, true)
}

const textDir = findDir(node, 'text')
if (textDir) {
return textDir.exp!
}

if (
node.children.every(
x =>
x.type === NodeTypes.TEXT ||
x.type === NodeTypes.COMMENT ||
x.type === NodeTypes.INTERPOLATION,
)
) {
const relevantNodes = collapseTextBetweenComments(node.children).filter(
x => x.type !== NodeTypes.COMMENT,
)
if (relevantNodes.length) {
const expressions = relevantNodes.map((x, i) => {
if (x.type === NodeTypes.TEXT) {
let content = x.content
if (i === 0) {
content = content.trimStart()
}
if (i === relevantNodes.length - 1) {
content = content.trimEnd()
}
return createSimpleExpression(content, true)
} else {
return x.content
}
})

if (expressions.length === 1) {
return expressions[0]
} else {
return createTemplateLiteral(expressions)
}
}
}

return createSimpleExpression(``, true)
}

function findValueBinding(node: PlainElementNode): ExpressionNode {
const valueBinding = findProp(node, 'value')
return valueBinding
Expand All @@ -198,3 +255,41 @@ function findValueBinding(node: PlainElementNode): ExpressionNode {
: createSimpleExpression(valueBinding.value!.content, true)
: createSimpleExpression(`null`, false)
}

function collapseTextBetweenComments<T extends TemplateChildNode>(
children: T[],
) {
const result: (T | TextNode)[] = []
let prevTextNode: TextNode | undefined
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.type === NodeTypes.TEXT) {
if (prevTextNode) {
const prevContent = prevTextNode.content
let thisContent = child.content
if (prevContent.endsWith(' ') && thisContent.startsWith(' ')) {
thisContent = thisContent.slice(1)
}
const combined: TextNode = {
...prevTextNode,
content: prevContent + thisContent,
}
prevTextNode = combined
} else {
prevTextNode = child
}
} else if (child.type === NodeTypes.COMMENT) {
continue
} else {
if (prevTextNode) {
result.push(prevTextNode)
prevTextNode = undefined
}
result.push(child)
}
}
if (prevTextNode) {
result.push(prevTextNode)
}
return result
}
22 changes: 22 additions & 0 deletions packages/server-renderer/__tests__/ssrDirectives.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,28 @@ describe('ssr: directives', () => {
`<select><option value="0"></option><option value="1" selected></option></select>`,
)

expect(
await renderToString(
createApp({
data: () => ({ model: 'f0o', zero: 0 }),
template: `<select v-model="model"><option>f{{ zero }}o</option><option>bar</option></select>`,
}),
),
).toBe(
`<select><option selected>f0o</option><option>bar</option></select>`,
)

expect(
await renderToString(
createApp({
data: () => ({ model: 'foo', opt1Val: 'foo', opt2Val: 'bar' }),
template: `<select v-model="model"><option v-text="opt1Val"></option><option v-text="opt2Val"></option></select>`,
}),
),
).toBe(
`<select><option selected>foo</option><option>bar</option></select>`,
)

expect(
await renderToString(
createApp({
Expand Down
Loading