type: NodeTypes.COMPOUND_EXPRESSION
children: (
| SimpleExpressionNode
+ | CompoundExpressionNode
| InterpolationNode
| TextNode
| string
// "onUpdate:modelValue": $event => (foo = $event)
createObjectProperty(
eventName,
- createCompoundExpression([
- `$event => (`,
- ...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
- ` = $event)`
- ])
+ createCompoundExpression([`$event => (`, exp, ` = $event)`])
)
]
const modifiersKey = arg
? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
? `${arg.content}Modifiers`
- : createCompoundExpression([
- ...(arg.type === NodeTypes.SIMPLE_EXPRESSION
- ? [arg]
- : arg.children),
- ' + "Modifiers"'
- ])
+ : createCompoundExpression([arg, ' + "Modifiers"'])
: `modelModifiers`
props.push(
createObjectProperty(
// wrap inline statement in a function expression
exp = createCompoundExpression([
`$event => ${hasMultipleStatements ? `{` : `(`}`,
- ...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
+ exp,
hasMultipleStatements ? `}` : `)`
])
}
X_V_MODEL_ON_INVALID_ELEMENT,
X_V_MODEL_ARG_ON_ELEMENT,
X_V_MODEL_ON_FILE_INPUT_ELEMENT,
+ X_V_MODEL_UNNECESSARY_VALUE,
X_V_SHOW_NO_EXPRESSION,
__EXTEND_POINT__
}
[DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT]: `v-model can only be used on <input>, <textarea> and <select> elements.`,
[DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT]: `v-model argument is not supported on plain elements.`,
[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.`,
+ [DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE]: `Unnecessary value binding used alongside v-model. It will interfere with v-model's behavior.`,
[DOMErrorCodes.X_V_SHOW_NO_EXPRESSION]: `v-show is missing expression.`
}
}
export { transformStyle } from './transforms/transformStyle'
-export { DOMErrorCodes } from './errors'
+export { createDOMCompilerError, DOMErrorCodes } from './errors'
export * from '@vue/compiler-core'
export const transformModel: DirectiveTransform = (dir, node, context) => {
const baseResult = baseTransform(dir, node, context)
- // base transform has errors
- if (!baseResult.props.length) {
+ // base transform has errors OR component v-model (only need props)
+ if (!baseResult.props.length || node.tagType === ElementTypes.COMPONENT) {
return baseResult
}
- const { tag, tagType } = node
- if (tagType === ElementTypes.ELEMENT) {
- if (dir.arg) {
+ if (dir.arg) {
+ context.onError(
+ createDOMCompilerError(
+ DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
+ dir.arg.loc
+ )
+ )
+ }
+
+ function checkDuplicatedValue() {
+ const value = findProp(node, 'value')
+ if (value) {
context.onError(
createDOMCompilerError(
- DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
- dir.arg.loc
+ DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
+ value.loc
)
)
}
+ }
- if (tag === 'input' || tag === 'textarea' || tag === 'select') {
- let directiveToUse = V_MODEL_TEXT
- let isInvalidType = false
- if (tag === 'input') {
- const type = findProp(node, `type`)
- if (type) {
- if (type.type === NodeTypes.DIRECTIVE) {
- // :type="foo"
- directiveToUse = V_MODEL_DYNAMIC
- } else if (type.value) {
- switch (type.value.content) {
- case 'radio':
- directiveToUse = V_MODEL_RADIO
- break
- case 'checkbox':
- directiveToUse = V_MODEL_CHECKBOX
- break
- case 'file':
- isInvalidType = true
- context.onError(
- createDOMCompilerError(
- DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
- dir.loc
- )
+ const { tag } = node
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') {
+ let directiveToUse = V_MODEL_TEXT
+ let isInvalidType = false
+ if (tag === 'input') {
+ const type = findProp(node, `type`)
+ if (type) {
+ if (type.type === NodeTypes.DIRECTIVE) {
+ // :type="foo"
+ directiveToUse = V_MODEL_DYNAMIC
+ } else if (type.value) {
+ switch (type.value.content) {
+ case 'radio':
+ directiveToUse = V_MODEL_RADIO
+ break
+ case 'checkbox':
+ directiveToUse = V_MODEL_CHECKBOX
+ break
+ case 'file':
+ isInvalidType = true
+ context.onError(
+ createDOMCompilerError(
+ DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
+ dir.loc
)
- break
- }
+ )
+ break
+ default:
+ // text type
+ __DEV__ && checkDuplicatedValue()
+ break
}
}
- } else if (tag === 'select') {
- directiveToUse = V_MODEL_SELECT
- }
- // inject runtime directive
- // by returning the helper symbol via needRuntime
- // the import will replaced a resolveDirective call.
- if (!isInvalidType) {
- baseResult.needRuntime = context.helper(directiveToUse)
+ } else {
+ // text type
+ __DEV__ && checkDuplicatedValue()
}
- } else {
- context.onError(
- createDOMCompilerError(
- DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
- dir.loc
- )
- )
+ } else if (tag === 'select') {
+ directiveToUse = V_MODEL_SELECT
+ } else if (tag === 'textarea') {
+ __DEV__ && checkDuplicatedValue()
}
+ // inject runtime directive
+ // by returning the helper symbol via needRuntime
+ // the import will replaced a resolveDirective call.
+ if (!isInvalidType) {
+ baseResult.needRuntime = context.helper(directiveToUse)
+ }
+ } else {
+ context.onError(
+ createDOMCompilerError(
+ DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
+ dir.loc
+ )
+ )
}
+
return baseResult
}
--- /dev/null
+import { compile } from '../src'
+
+describe('ssr: v-model', () => {
+ test('<input> (text types)', () => {
+ expect(compile(`<input v-model="bar">`).code).toMatchInlineSnapshot(`
+ "const { _renderAttr } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ _push(\`<input\${_renderAttr(\\"value\\", _ctx.bar)}>\`)
+ }"
+ `)
+
+ expect(compile(`<input type="email" v-model="bar">`).code)
+ .toMatchInlineSnapshot(`
+ "const { _renderAttr } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ _push(\`<input type=\\"email\\"\${_renderAttr(\\"value\\", _ctx.bar)}>\`)
+ }"
+ `)
+ })
+
+ test('<input type="radio">', () => {
+ expect(compile(`<input type="radio" value="foo" v-model="bar">`).code)
+ .toMatchInlineSnapshot(`
+ "const { _looseEqual } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ _push(\`<input type=\\"radio\\" value=\\"foo\\"\${(_looseEqual(_ctx.bar, \\"foo\\")) ? \\" checked\\" : \\"\\"}>\`)
+ }"
+ `)
+ })
+
+ test('<input type="checkbox"', () => {
+ expect(compile(`<input type="checkbox" v-model="bar">`).code)
+ .toMatchInlineSnapshot(`
+ "const { _looseContain } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ _push(\`<input type=\\"checkbox\\"\${((Array.isArray(_ctx.bar))
+ ? _looseContain(_ctx.bar, null)
+ : _ctx.bar) ? \\" checked\\" : \\"\\"}>\`)
+ }"
+ `)
+
+ expect(compile(`<input type="checkbox" value="foo" v-model="bar">`).code)
+ .toMatchInlineSnapshot(`
+ "const { _looseContain } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ _push(\`<input type=\\"checkbox\\" value=\\"foo\\"\${((Array.isArray(_ctx.bar))
+ ? _looseContain(_ctx.bar, \\"foo\\")
+ : _ctx.bar) ? \\" checked\\" : \\"\\"}>\`)
+ }"
+ `)
+ })
+
+ test('<textarea>', () => {
+ expect(compile(`<textarea v-model="foo">bar</textarea>`).code)
+ .toMatchInlineSnapshot(`
+ "const { _interpolate } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ _push(\`<textarea>\${_interpolate(_ctx.foo)}</textarea>\`)
+ }"
+ `)
+ })
+})
export const SSR_RENDER_ATTR = Symbol(`renderAttr`)
export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`renderDynamicAttr`)
export const SSR_RENDER_LIST = Symbol(`renderList`)
+export const SSR_LOOSE_EQUAL = Symbol(`looseEqual`)
+export const SSR_LOOSE_CONTAIN = Symbol(`looseContain`)
export const ssrHelpers = {
[SSR_INTERPOLATE]: `_interpolate`,
[SSR_RENDER_ATTRS]: `_renderAttrs`,
[SSR_RENDER_ATTR]: `_renderAttr`,
[SSR_RENDER_DYNAMIC_ATTR]: `_renderDynamicAttr`,
- [SSR_RENDER_LIST]: `_renderList`
+ [SSR_RENDER_LIST]: `_renderList`,
+ [SSR_LOOSE_EQUAL]: `_looseEqual`,
+ [SSR_LOOSE_CONTAIN]: `_looseContain`
}
// Note: these are helpers imported from @vue/server-renderer
-import { DirectiveTransform } from '@vue/compiler-dom'
+import {
+ DirectiveTransform,
+ ElementTypes,
+ transformModel,
+ findProp,
+ NodeTypes,
+ createDOMCompilerError,
+ DOMErrorCodes,
+ Property,
+ createObjectProperty,
+ createSimpleExpression,
+ createCallExpression,
+ PlainElementNode,
+ ExpressionNode,
+ createConditionalExpression,
+ createInterpolation
+} from '@vue/compiler-dom'
+import { SSR_LOOSE_EQUAL, SSR_LOOSE_CONTAIN } from '../runtimeHelpers'
export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
- return {
- props: []
+ const model = dir.exp!
+
+ function checkDuplicatedValue() {
+ const value = findProp(node, 'value')
+ if (value) {
+ context.onError(
+ createDOMCompilerError(
+ DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
+ value.loc
+ )
+ )
+ }
+ }
+
+ if (node.tagType === ElementTypes.ELEMENT) {
+ let props: Property[] = []
+ const defaultProps = [
+ // default value binding for text type inputs
+ createObjectProperty(createSimpleExpression(`value`, true), model)
+ ]
+ if (node.tag === 'input') {
+ const type = findProp(node, 'type')
+ if (type) {
+ if (type.type === NodeTypes.DIRECTIVE) {
+ // dynamic type
+ // TODO
+ } else if (type.value) {
+ // static type
+ switch (type.value.content) {
+ case 'radio':
+ props = [
+ createObjectProperty(
+ createSimpleExpression(`checked`, true),
+ createCallExpression(context.helper(SSR_LOOSE_EQUAL), [
+ model,
+ findValueBinding(node)
+ ])
+ )
+ ]
+ break
+ case 'checkbox':
+ const value = findValueBinding(node)
+ props = [
+ createObjectProperty(
+ createSimpleExpression(`checked`, true),
+ createConditionalExpression(
+ createCallExpression(`Array.isArray`, [model]),
+ createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [
+ model,
+ value
+ ]),
+ model
+ )
+ )
+ ]
+ break
+ case 'file':
+ context.onError(
+ createDOMCompilerError(
+ DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
+ dir.loc
+ )
+ )
+ break
+ default:
+ checkDuplicatedValue()
+ props = defaultProps
+ break
+ }
+ }
+ } else {
+ checkDuplicatedValue()
+ props = defaultProps
+ }
+ } else if (node.tag === 'textarea') {
+ checkDuplicatedValue()
+ node.children = [createInterpolation(model, model.loc)]
+ } else if (node.tag === 'select') {
+ // TODO
+ } else {
+ context.onError(
+ createDOMCompilerError(
+ DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
+ dir.loc
+ )
+ )
+ }
+
+ return { props }
+ } else {
+ // component v-model
+ 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)
+}
import {
DirectiveTransform,
- createCompilerError,
DOMErrorCodes,
createObjectProperty,
createSimpleExpression,
createConditionalExpression,
- createObjectExpression
+ createObjectExpression,
+ createDOMCompilerError
} from '@vue/compiler-dom'
export const ssrTransformShow: DirectiveTransform = (dir, node, context) => {
if (!dir.exp) {
- context.onError(createCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION))
+ context.onError(
+ createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION)
+ )
}
return {
props: [
warn
} from '@vue/runtime-core'
import { addEventListener } from '../modules/events'
-import { isArray, isObject } from '@vue/shared'
+import { isArray, looseEqual, looseIndexOf } from '@vue/shared'
const getModelAssigner = (vnode: VNode): ((value: any) => void) =>
vnode.props!['onUpdate:modelValue']
}
}
-function looseEqual(a: any, b: any): boolean {
- if (a === b) return true
- const isObjectA = isObject(a)
- const isObjectB = isObject(b)
- if (isObjectA && isObjectB) {
- try {
- const isArrayA = isArray(a)
- const isArrayB = isArray(b)
- if (isArrayA && isArrayB) {
- return (
- a.length === b.length &&
- a.every((e: any, i: any) => looseEqual(e, b[i]))
- )
- } else if (a instanceof Date && b instanceof Date) {
- return a.getTime() === b.getTime()
- } else if (!isArrayA && !isArrayB) {
- const keysA = Object.keys(a)
- const keysB = Object.keys(b)
- return (
- keysA.length === keysB.length &&
- keysA.every(key => looseEqual(a[key], b[key]))
- )
- } else {
- /* istanbul ignore next */
- return false
- }
- } catch (e) {
- /* istanbul ignore next */
- return false
- }
- } else if (!isObjectA && !isObjectB) {
- return String(a) === String(b)
- } else {
- return false
- }
-}
-
-function looseIndexOf(arr: any[], val: any): number {
- return arr.findIndex(item => looseEqual(item, val))
-}
-
// retrieve raw value set via :value bindings
function getValue(el: HTMLOptionElement | HTMLInputElement) {
return '_value' in el ? (el as any)._value : el.value
} from './helpers/renderAttrs'
export { interpolate as _interpolate } from './helpers/interpolate'
export { renderList as _renderList } from './helpers/renderList'
+
+// v-model helpers
+import { looseEqual, looseIndexOf } from '@vue/shared'
+export const _looseEqual = looseEqual as (a: unknown, b: unknown) => boolean
+export const _looseContain = (arr: unknown[], value: unknown): boolean =>
+ looseIndexOf(arr, value) > -1
export * from './domTagConfig'
export * from './domAttrConfig'
export * from './escapeHtml'
+export * from './looseEqual'
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
? Object.freeze({})
--- /dev/null
+import { isObject, isArray } from './'
+
+export function looseEqual(a: any, b: any): boolean {
+ if (a === b) return true
+ const isObjectA = isObject(a)
+ const isObjectB = isObject(b)
+ if (isObjectA && isObjectB) {
+ try {
+ const isArrayA = isArray(a)
+ const isArrayB = isArray(b)
+ if (isArrayA && isArrayB) {
+ return (
+ a.length === b.length &&
+ a.every((e: any, i: any) => looseEqual(e, b[i]))
+ )
+ } else if (a instanceof Date && b instanceof Date) {
+ return a.getTime() === b.getTime()
+ } else if (!isArrayA && !isArrayB) {
+ const keysA = Object.keys(a)
+ const keysB = Object.keys(b)
+ return (
+ keysA.length === keysB.length &&
+ keysA.every(key => looseEqual(a[key], b[key]))
+ )
+ } else {
+ /* istanbul ignore next */
+ return false
+ }
+ } catch (e) {
+ /* istanbul ignore next */
+ return false
+ }
+ } else if (!isObjectA && !isObjectB) {
+ return String(a) === String(b)
+ } else {
+ return false
+ }
+}
+
+export function looseIndexOf(arr: any[], val: any): number {
+ return arr.findIndex(item => looseEqual(item, val))
+}
err.loc.end.offset
)
warn(codeFrame ? `${message}\n${codeFrame}` : message)
+ } else {
+ throw err
}
},
...options