Browse Source

v-model binding with array. (fix #3958,#3979) (#3988)

* fix v-model with array binding

* add mutli selects test case

* add test case. v-bind with array

* add comments

* code refactor
dev
defcc 8 years ago
committed by Evan You
parent
commit
3105661662
  1. 24
      src/platforms/web/compiler/directives/model.js
  2. 84
      src/platforms/web/util/model.js
  3. 11
      test/unit/features/directives/model-component.spec.js
  4. 40
      test/unit/features/directives/model-radio.spec.js
  5. 38
      test/unit/features/directives/model-select.spec.js
  6. 41
      test/unit/features/directives/model-text.spec.js

24
src/platforms/web/compiler/directives/model.js

@ -2,6 +2,7 @@
import { isIE } from 'core/util/env'
import { addHandler, addProp, getBindingAttr } from 'compiler/helpers'
import parseModel from 'web/util/model'
let warn
@ -79,7 +80,7 @@ function genRadioModel (el: ASTElement, value: string) {
}
const valueBinding = getBindingAttr(el, 'value') || 'null'
addProp(el, 'checked', `_q(${value},${valueBinding})`)
addHandler(el, 'change', `${value}=${valueBinding}`, null, true)
addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true)
}
function genDefaultModel (
@ -114,8 +115,8 @@ function genDefaultModel (
? `$event.target.value${trim ? '.trim()' : ''}`
: `$event`
let code = number || type === 'number'
? `${value}=_n(${valueExpression})`
: `${value}=${valueExpression}`
? genAssignmentCode(value, `_n(${valueExpression})`)
: genAssignmentCode(value, valueExpression)
if (isNative && needCompositionGuard) {
code = `if($event.target.composing)return;${code}`
}
@ -136,10 +137,13 @@ function genSelect (el: ASTElement, value: string) {
if (process.env.NODE_ENV !== 'production') {
el.children.some(checkOptionWarning)
}
const code = `${value}=Array.prototype.filter` +
const assignment = `Array.prototype.filter` +
`.call($event.target.options,function(o){return o.selected})` +
`.map(function(o){return "_value" in o ? o._value : o.value})` +
(el.attrsMap.multiple == null ? '[0]' : '')
const code = genAssignmentCode(value, assignment)
addHandler(el, 'change', code, null, true)
}
@ -156,3 +160,15 @@ function checkOptionWarning (option: any): boolean {
}
return false
}
function genAssignmentCode (value: string, assignment: string): string {
const modelRs = parseModel(value)
if (modelRs.idx === null) {
return `${value}=${assignment}`
} else {
return `var $$exp = ${modelRs.exp}, $$idx = ${modelRs.idx};` +
`if (!Array.isArray($$exp)){` +
`${value}=${assignment}}` +
`else{$$exp.splice($$idx, 1, ${assignment})}`
}
}

84
src/platforms/web/util/model.js

@ -0,0 +1,84 @@
/* @flow */
let len, str, chr, index, expressionPos, expressionEndPos
/**
* parse directive model to do the array update transform. a[idx] = val => $$a.splice($$idx, 1, val)
*
* for loop possible cases:
*
* - test
* - test[idx]
* - test[test1[idx]]
* - test["a"][idx]
* - xxx.test[a[a].test1[idx]]
* - test.xxx.a["asa"][test1[idx]]
*
*/
export default function parseModel (val: string): Object {
str = val
len = str.length
index = expressionPos = expressionEndPos = 0
if (val.indexOf('[') < 0) {
return {
exp: val,
idx: null
}
}
while (!eof()) {
chr = next()
if (isStringStart(chr)) {
parseString(chr)
} else if (chr === 0x5B) {
parseBracket(chr)
}
}
return {
exp: val.substring(0, expressionPos),
idx: val.substring(expressionPos + 1, expressionEndPos)
}
}
function next (): number {
return str.charCodeAt(++index)
}
function eof (): boolean {
return index >= len
}
function isStringStart (chr: number): boolean {
return chr === 0x22 || chr === 0x27
}
function parseBracket (chr: number): void {
let inBracket = 1
expressionPos = index
while (!eof()) {
chr = next()
if (isStringStart(chr)) {
parseString(chr)
continue
}
if (chr === 0x5B) inBracket++
if (chr === 0x5D) inBracket--
if (inBracket === 0) {
expressionEndPos = index
break
}
}
}
function parseString (chr: number): void {
const stringQuote = chr
while (!eof()) {
chr = next()
if (chr === stringQuote) {
break
}
}
}

11
test/unit/features/directives/model-component.spec.js

@ -2,14 +2,18 @@ import Vue from 'vue'
describe('Directive v-model component', () => {
it('should work', done => {
const spy = jasmine.createSpy()
const vm = new Vue({
data: {
msg: 'hello'
msg: ['hello']
},
watch: {
msg: spy
},
template: `
<div>
<p>{{ msg }}</p>
<validate v-model="msg">
<validate v-model="msg[0]">
<input type="text">
</validate>
</div>
@ -40,7 +44,8 @@ describe('Directive v-model component', () => {
input.value = 'world'
triggerEvent(input, 'input')
}).then(() => {
expect(vm.msg).toBe('world')
expect(vm.msg).toEqual(['world'])
expect(spy).toHaveBeenCalled()
}).then(() => {
document.body.removeChild(vm.$el)
vm.$destroy()

40
test/unit/features/directives/model-radio.spec.js

@ -85,6 +85,46 @@ describe('Directive v-model radio', () => {
}).then(done)
})
it('multiple radios ', (done) => {
const spy = jasmine.createSpy()
const vm = new Vue({
data: {
selections: ['a', '1'],
radioList: [
{
name: 'questionA',
data: ['a', 'b', 'c']
},
{
name: 'questionB',
data: ['1', '2']
}
]
},
watch: {
selections: spy
},
template:
'<div>' +
'<div v-for="(radioGroup, idx) in radioList">' +
'<div>' +
'<span v-for="(item, index) in radioGroup.data">' +
'<input :name="radioGroup.name" type="radio" :value="item" v-model="selections[idx]" :id="idx"/>' +
'<label>{{item}}</label>' +
'</span>' +
'</div>' +
'</div>' +
'</div>'
}).$mount()
document.body.appendChild(vm.$el)
var inputs = vm.$el.getElementsByTagName('input')
inputs[1].click()
waitForUpdate(() => {
expect(vm.selections).toEqual(['b', '1'])
expect(spy).toHaveBeenCalled()
}).then(done)
})
it('warn inline checked', () => {
const vm = new Vue({
template: `<input v-model="test" type="radio" value="1" checked>`,

38
test/unit/features/directives/model-select.spec.js

@ -263,6 +263,44 @@ describe('Directive v-model select', () => {
}).then(done)
})
it('multiple selects', (done) => {
const spy = jasmine.createSpy()
const vm = new Vue({
data: {
selections: ['', ''],
selectBoxes: [
[
{ value: 'foo', text: 'foo' },
{ value: 'bar', text: 'bar' }
],
[
{ value: 'day', text: 'day' },
{ value: 'night', text: 'night' }
]
]
},
watch: {
selections: spy
},
template:
'<div>' +
'<select v-for="(item, index) in selectBoxes" v-model="selections[index]">' +
'<option v-for="element in item" v-bind:value="element.value" v-text="element.text"></option>' +
'</select>' +
'<span ref="rs">{{selections}}</span>' +
'</div>'
}).$mount()
document.body.appendChild(vm.$el)
var selects = vm.$el.getElementsByTagName('select')
var select0 = selects[0]
select0.options[0].selected = true
triggerEvent(select0, 'change')
waitForUpdate(() => {
expect(spy).toHaveBeenCalled()
expect(vm.selections).toEqual(['foo', ''])
}).then(done)
})
it('should warn inline selected', () => {
const vm = new Vue({
data: {

41
test/unit/features/directives/model-text.spec.js

@ -64,6 +64,47 @@ describe('Directive v-model text', () => {
expect(vm.test).toBe('what')
})
it('multiple inputs', (done) => {
const spy = jasmine.createSpy()
const vm = new Vue({
data: {
selections: [[1, 2, 3], [4, 5]],
inputList: [
{
name: 'questionA',
data: ['a', 'b', 'c']
},
{
name: 'questionB',
data: ['1', '2']
}
]
},
watch: {
selections: spy
},
template:
'<div>' +
'<div v-for="(inputGroup, idx) in inputList">' +
'<div>' +
'<span v-for="(item, index) in inputGroup.data">' +
'<input v-bind:name="item" type="text" v-model.number="selections[idx][index]" v-bind:id="idx+\'-\'+index"/>' +
'<label>{{item}}</label>' +
'</span>' +
'</div>' +
'</div>' +
'<span ref="rs">{{selections}}</span>' +
'</div>'
}).$mount()
var inputs = vm.$el.getElementsByTagName('input')
inputs[1].value = 'test'
triggerEvent(inputs[1], 'input')
waitForUpdate(() => {
expect(spy).toHaveBeenCalled()
expect(vm.selections).toEqual([[1, 'test', 3], [4, 5]])
}).then(done)
})
if (isIE9) {
it('IE9 selectionchange', done => {
const vm = new Vue({

Loading…
Cancel
Save