Skip to content

Commit c952321

Browse files
committed
wip(compiler-ssr): v-model static types + textarea
1 parent fd470e0 commit c952321

File tree

15 files changed

+324
-110
lines changed

15 files changed

+324
-110
lines changed

packages/compiler-core/src/ast.ts

+1
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ export interface CompoundExpressionNode extends Node {
204204
type: NodeTypes.COMPOUND_EXPRESSION
205205
children: (
206206
| SimpleExpressionNode
207+
| CompoundExpressionNode
207208
| InterpolationNode
208209
| TextNode
209210
| string

packages/compiler-core/src/transforms/vModel.ts

+2-11
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
5656
// "onUpdate:modelValue": $event => (foo = $event)
5757
createObjectProperty(
5858
eventName,
59-
createCompoundExpression([
60-
`$event => (`,
61-
...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
62-
` = $event)`
63-
])
59+
createCompoundExpression([`$event => (`, exp, ` = $event)`])
6460
)
6561
]
6662

@@ -82,12 +78,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
8278
const modifiersKey = arg
8379
? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
8480
? `${arg.content}Modifiers`
85-
: createCompoundExpression([
86-
...(arg.type === NodeTypes.SIMPLE_EXPRESSION
87-
? [arg]
88-
: arg.children),
89-
' + "Modifiers"'
90-
])
81+
: createCompoundExpression([arg, ' + "Modifiers"'])
9182
: `modelModifiers`
9283
props.push(
9384
createObjectProperty(

packages/compiler-core/src/transforms/vOn.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const transformOn: DirectiveTransform = (
8787
// wrap inline statement in a function expression
8888
exp = createCompoundExpression([
8989
`$event => ${hasMultipleStatements ? `{` : `(`}`,
90-
...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
90+
exp,
9191
hasMultipleStatements ? `}` : `)`
9292
])
9393
}

packages/compiler-dom/src/errors.ts

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const enum DOMErrorCodes {
2828
X_V_MODEL_ON_INVALID_ELEMENT,
2929
X_V_MODEL_ARG_ON_ELEMENT,
3030
X_V_MODEL_ON_FILE_INPUT_ELEMENT,
31+
X_V_MODEL_UNNECESSARY_VALUE,
3132
X_V_SHOW_NO_EXPRESSION,
3233
__EXTEND_POINT__
3334
}
@@ -40,5 +41,6 @@ export const DOMErrorMessages: { [code: number]: string } = {
4041
[DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT]: `v-model can only be used on <input>, <textarea> and <select> elements.`,
4142
[DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT]: `v-model argument is not supported on plain elements.`,
4243
[DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT]: `v-model cannot used on file inputs since they are read-only. Use a v-on:change listener instead.`,
44+
[DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE]: `Unnecessary value binding used alongside v-model. It will interfere with v-model's behavior.`,
4345
[DOMErrorCodes.X_V_SHOW_NO_EXPRESSION]: `v-show is missing expression.`
4446
}

packages/compiler-dom/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,5 @@ export function parse(template: string, options: ParserOptions = {}): RootNode {
5757
}
5858

5959
export { transformStyle } from './transforms/transformStyle'
60-
export { DOMErrorCodes } from './errors'
60+
export { createDOMCompilerError, DOMErrorCodes } from './errors'
6161
export * from '@vue/compiler-core'

packages/compiler-dom/src/transforms/vModel.ts

+68-48
Original file line numberDiff line numberDiff line change
@@ -16,68 +16,88 @@ import {
1616

1717
export const transformModel: DirectiveTransform = (dir, node, context) => {
1818
const baseResult = baseTransform(dir, node, context)
19-
// base transform has errors
20-
if (!baseResult.props.length) {
19+
// base transform has errors OR component v-model (only need props)
20+
if (!baseResult.props.length || node.tagType === ElementTypes.COMPONENT) {
2121
return baseResult
2222
}
2323

24-
const { tag, tagType } = node
25-
if (tagType === ElementTypes.ELEMENT) {
26-
if (dir.arg) {
24+
if (dir.arg) {
25+
context.onError(
26+
createDOMCompilerError(
27+
DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
28+
dir.arg.loc
29+
)
30+
)
31+
}
32+
33+
function checkDuplicatedValue() {
34+
const value = findProp(node, 'value')
35+
if (value) {
2736
context.onError(
2837
createDOMCompilerError(
29-
DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
30-
dir.arg.loc
38+
DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
39+
value.loc
3140
)
3241
)
3342
}
43+
}
3444

35-
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
36-
let directiveToUse = V_MODEL_TEXT
37-
let isInvalidType = false
38-
if (tag === 'input') {
39-
const type = findProp(node, `type`)
40-
if (type) {
41-
if (type.type === NodeTypes.DIRECTIVE) {
42-
// :type="foo"
43-
directiveToUse = V_MODEL_DYNAMIC
44-
} else if (type.value) {
45-
switch (type.value.content) {
46-
case 'radio':
47-
directiveToUse = V_MODEL_RADIO
48-
break
49-
case 'checkbox':
50-
directiveToUse = V_MODEL_CHECKBOX
51-
break
52-
case 'file':
53-
isInvalidType = true
54-
context.onError(
55-
createDOMCompilerError(
56-
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
57-
dir.loc
58-
)
45+
const { tag } = node
46+
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
47+
let directiveToUse = V_MODEL_TEXT
48+
let isInvalidType = false
49+
if (tag === 'input') {
50+
const type = findProp(node, `type`)
51+
if (type) {
52+
if (type.type === NodeTypes.DIRECTIVE) {
53+
// :type="foo"
54+
directiveToUse = V_MODEL_DYNAMIC
55+
} else if (type.value) {
56+
switch (type.value.content) {
57+
case 'radio':
58+
directiveToUse = V_MODEL_RADIO
59+
break
60+
case 'checkbox':
61+
directiveToUse = V_MODEL_CHECKBOX
62+
break
63+
case 'file':
64+
isInvalidType = true
65+
context.onError(
66+
createDOMCompilerError(
67+
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
68+
dir.loc
5969
)
60-
break
61-
}
70+
)
71+
break
72+
default:
73+
// text type
74+
__DEV__ && checkDuplicatedValue()
75+
break
6276
}
6377
}
64-
} else if (tag === 'select') {
65-
directiveToUse = V_MODEL_SELECT
66-
}
67-
// inject runtime directive
68-
// by returning the helper symbol via needRuntime
69-
// the import will replaced a resolveDirective call.
70-
if (!isInvalidType) {
71-
baseResult.needRuntime = context.helper(directiveToUse)
78+
} else {
79+
// text type
80+
__DEV__ && checkDuplicatedValue()
7281
}
73-
} else {
74-
context.onError(
75-
createDOMCompilerError(
76-
DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
77-
dir.loc
78-
)
79-
)
82+
} else if (tag === 'select') {
83+
directiveToUse = V_MODEL_SELECT
84+
} else if (tag === 'textarea') {
85+
__DEV__ && checkDuplicatedValue()
8086
}
87+
// inject runtime directive
88+
// by returning the helper symbol via needRuntime
89+
// the import will replaced a resolveDirective call.
90+
if (!isInvalidType) {
91+
baseResult.needRuntime = context.helper(directiveToUse)
92+
}
93+
} else {
94+
context.onError(
95+
createDOMCompilerError(
96+
DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
97+
dir.loc
98+
)
99+
)
81100
}
101+
82102
return baseResult
83103
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { compile } from '../src'
2+
3+
describe('ssr: v-model', () => {
4+
test('<input> (text types)', () => {
5+
expect(compile(`<input v-model="bar">`).code).toMatchInlineSnapshot(`
6+
"const { _renderAttr } = require(\\"@vue/server-renderer\\")
7+
8+
return function ssrRender(_ctx, _push, _parent) {
9+
_push(\`<input\${_renderAttr(\\"value\\", _ctx.bar)}>\`)
10+
}"
11+
`)
12+
13+
expect(compile(`<input type="email" v-model="bar">`).code)
14+
.toMatchInlineSnapshot(`
15+
"const { _renderAttr } = require(\\"@vue/server-renderer\\")
16+
17+
return function ssrRender(_ctx, _push, _parent) {
18+
_push(\`<input type=\\"email\\"\${_renderAttr(\\"value\\", _ctx.bar)}>\`)
19+
}"
20+
`)
21+
})
22+
23+
test('<input type="radio">', () => {
24+
expect(compile(`<input type="radio" value="foo" v-model="bar">`).code)
25+
.toMatchInlineSnapshot(`
26+
"const { _looseEqual } = require(\\"@vue/server-renderer\\")
27+
28+
return function ssrRender(_ctx, _push, _parent) {
29+
_push(\`<input type=\\"radio\\" value=\\"foo\\"\${(_looseEqual(_ctx.bar, \\"foo\\")) ? \\" checked\\" : \\"\\"}>\`)
30+
}"
31+
`)
32+
})
33+
34+
test('<input type="checkbox"', () => {
35+
expect(compile(`<input type="checkbox" v-model="bar">`).code)
36+
.toMatchInlineSnapshot(`
37+
"const { _looseContain } = require(\\"@vue/server-renderer\\")
38+
39+
return function ssrRender(_ctx, _push, _parent) {
40+
_push(\`<input type=\\"checkbox\\"\${((Array.isArray(_ctx.bar))
41+
? _looseContain(_ctx.bar, null)
42+
: _ctx.bar) ? \\" checked\\" : \\"\\"}>\`)
43+
}"
44+
`)
45+
46+
expect(compile(`<input type="checkbox" value="foo" v-model="bar">`).code)
47+
.toMatchInlineSnapshot(`
48+
"const { _looseContain } = require(\\"@vue/server-renderer\\")
49+
50+
return function ssrRender(_ctx, _push, _parent) {
51+
_push(\`<input type=\\"checkbox\\" value=\\"foo\\"\${((Array.isArray(_ctx.bar))
52+
? _looseContain(_ctx.bar, \\"foo\\")
53+
: _ctx.bar) ? \\" checked\\" : \\"\\"}>\`)
54+
}"
55+
`)
56+
})
57+
58+
test('<textarea>', () => {
59+
expect(compile(`<textarea v-model="foo">bar</textarea>`).code)
60+
.toMatchInlineSnapshot(`
61+
"const { _interpolate } = require(\\"@vue/server-renderer\\")
62+
63+
return function ssrRender(_ctx, _push, _parent) {
64+
_push(\`<textarea>\${_interpolate(_ctx.foo)}</textarea>\`)
65+
}"
66+
`)
67+
})
68+
})

packages/compiler-ssr/src/runtimeHelpers.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export const SSR_RENDER_ATTRS = Symbol(`renderAttrs`)
99
export const SSR_RENDER_ATTR = Symbol(`renderAttr`)
1010
export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`renderDynamicAttr`)
1111
export const SSR_RENDER_LIST = Symbol(`renderList`)
12+
export const SSR_LOOSE_EQUAL = Symbol(`looseEqual`)
13+
export const SSR_LOOSE_CONTAIN = Symbol(`looseContain`)
1214

1315
export const ssrHelpers = {
1416
[SSR_INTERPOLATE]: `_interpolate`,
@@ -19,7 +21,9 @@ export const ssrHelpers = {
1921
[SSR_RENDER_ATTRS]: `_renderAttrs`,
2022
[SSR_RENDER_ATTR]: `_renderAttr`,
2123
[SSR_RENDER_DYNAMIC_ATTR]: `_renderDynamicAttr`,
22-
[SSR_RENDER_LIST]: `_renderList`
24+
[SSR_RENDER_LIST]: `_renderList`,
25+
[SSR_LOOSE_EQUAL]: `_looseEqual`,
26+
[SSR_LOOSE_CONTAIN]: `_looseContain`
2327
}
2428

2529
// Note: these are helpers imported from @vue/server-renderer

0 commit comments

Comments
 (0)