import config from 'core/config' import { addHandler, addProp, getBindingAttr } from 'compiler/helpers' import { genComponentModel, genAssignmentCode } from 'compiler/directives/model' import { ASTDirective, ASTElement, ASTModifiers } from 'types/compiler' let warn // in some cases, the event used has to be determined at runtime // so we used some reserved tokens during compile. export const RANGE_TOKEN = '__r' export const CHECKBOX_RADIO_TOKEN = '__c' export default function model( el: ASTElement, dir: ASTDirective, _warn: Function ): boolean | undefined { warn = _warn const value = dir.value const modifiers = dir.modifiers const tag = el.tag const type = el.attrsMap.type if (__DEV__) { // inputs with type="file" are read only and setting the input's // value will throw an error. if (tag === 'input' && type === 'file') { warn( `<${el.tag} v-model="${value}" type="file">:\n` + `File inputs are read only. Use a v-on:change listener instead.`, el.rawAttrsMap['v-model'] ) } } if (el.component) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false } else if (tag === 'select') { genSelect(el, value, modifiers) } else if (tag === 'input' && type === 'checkbox') { genCheckboxModel(el, value, modifiers) } else if (tag === 'input' && type === 'radio') { genRadioModel(el, value, modifiers) } else if (tag === 'input' || tag === 'textarea') { genDefaultModel(el, value, modifiers) } else if (!config.isReservedTag(tag)) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false } else if (__DEV__) { warn( `<${el.tag} v-model="${value}">: ` + `v-model is not supported on this element type. ` + "If you are working with contenteditable, it's recommended to " + 'wrap a library dedicated for that purpose inside a custom component.', el.rawAttrsMap['v-model'] ) } // ensure runtime directive metadata return true } function genCheckboxModel( el: ASTElement, value: string, modifiers?: ASTModifiers | null ) { const number = modifiers && modifiers.number const valueBinding = getBindingAttr(el, 'value') || 'null' const trueValueBinding = getBindingAttr(el, 'true-value') || 'true' const falseValueBinding = getBindingAttr(el, 'false-value') || 'false' addProp( el, 'checked', `Array.isArray(${value})` + `?_i(${value},${valueBinding})>-1` + (trueValueBinding === 'true' ? `:(${value})` : `:_q(${value},${trueValueBinding})`) ) addHandler( el, 'change', `var $$a=${value},` + '$$el=$event.target,' + `$$c=$$el.checked?(${trueValueBinding}):(${falseValueBinding});` + 'if(Array.isArray($$a)){' + `var $$v=${number ? '_n(' + valueBinding + ')' : valueBinding},` + '$$i=_i($$a,$$v);' + `if($$el.checked){$$i<0&&(${genAssignmentCode( value, '$$a.concat([$$v])' )})}` + `else{$$i>-1&&(${genAssignmentCode( value, '$$a.slice(0,$$i).concat($$a.slice($$i+1))' )})}` + `}else{${genAssignmentCode(value, '$$c')}}`, null, true ) } function genRadioModel( el: ASTElement, value: string, modifiers?: ASTModifiers | null ) { const number = modifiers && modifiers.number let valueBinding = getBindingAttr(el, 'value') || 'null' valueBinding = number ? `_n(${valueBinding})` : valueBinding addProp(el, 'checked', `_q(${value},${valueBinding})`) addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true) } function genSelect( el: ASTElement, value: string, modifiers?: ASTModifiers | null ) { const number = modifiers && modifiers.number const selectedVal = `Array.prototype.filter` + `.call($event.target.options,function(o){return o.selected})` + `.map(function(o){var val = "_value" in o ? o._value : o.value;` + `return ${number ? '_n(val)' : 'val'}})` const assignment = '$event.target.multiple ? $$selectedVal : $$selectedVal[0]' let code = `var $$selectedVal = ${selectedVal};` code = `${code} ${genAssignmentCode(value, assignment)}` addHandler(el, 'change', code, null, true) } function genDefaultModel( el: ASTElement, value: string, modifiers?: ASTModifiers | null ): boolean | void { const type = el.attrsMap.type // warn if v-bind:value conflicts with v-model // except for inputs with v-bind:type if (__DEV__) { const value = el.attrsMap['v-bind:value'] || el.attrsMap[':value'] const typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'] if (value && !typeBinding) { const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value' warn( `${binding}="${value}" conflicts with v-model on the same element ` + 'because the latter already expands to a value binding internally', el.rawAttrsMap[binding] ) } } const { lazy, number, trim } = modifiers || {} const needCompositionGuard = !lazy && type !== 'range' const event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input' let valueExpression = '$event.target.value' if (trim) { valueExpression = `$event.target.value.trim()` } if (number) { valueExpression = `_n(${valueExpression})` } let code = genAssignmentCode(value, valueExpression) if (needCompositionGuard) { code = `if($event.target.composing)return;${code}` } addProp(el, 'value', `(${value})`) addHandler(el, event, code, null, true) if (trim || number) { addHandler(el, 'blur', '$forceUpdate()') } }