Skip to content

Commit 3105661

Browse files
defccyyx990803
authored andcommitted
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
1 parent 5f8ae40 commit 3105661

File tree

6 files changed

+231
-7
lines changed

6 files changed

+231
-7
lines changed

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { isIE } from 'core/util/env'
44
import { addHandler, addProp, getBindingAttr } from 'compiler/helpers'
5+
import parseModel from 'web/util/model'
56

67
let warn
78

@@ -79,7 +80,7 @@ function genRadioModel (el: ASTElement, value: string) {
7980
}
8081
const valueBinding = getBindingAttr(el, 'value') || 'null'
8182
addProp(el, 'checked', `_q(${value},${valueBinding})`)
82-
addHandler(el, 'change', `${value}=${valueBinding}`, null, true)
83+
addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true)
8384
}
8485

8586
function genDefaultModel (
@@ -114,8 +115,8 @@ function genDefaultModel (
114115
? `$event.target.value${trim ? '.trim()' : ''}`
115116
: `$event`
116117
let code = number || type === 'number'
117-
? `${value}=_n(${valueExpression})`
118-
: `${value}=${valueExpression}`
118+
? genAssignmentCode(value, `_n(${valueExpression})`)
119+
: genAssignmentCode(value, valueExpression)
119120
if (isNative && needCompositionGuard) {
120121
code = `if($event.target.composing)return;${code}`
121122
}
@@ -136,10 +137,13 @@ function genSelect (el: ASTElement, value: string) {
136137
if (process.env.NODE_ENV !== 'production') {
137138
el.children.some(checkOptionWarning)
138139
}
139-
const code = `${value}=Array.prototype.filter` +
140+
141+
const assignment = `Array.prototype.filter` +
140142
`.call($event.target.options,function(o){return o.selected})` +
141143
`.map(function(o){return "_value" in o ? o._value : o.value})` +
142144
(el.attrsMap.multiple == null ? '[0]' : '')
145+
146+
const code = genAssignmentCode(value, assignment)
143147
addHandler(el, 'change', code, null, true)
144148
}
145149

@@ -156,3 +160,15 @@ function checkOptionWarning (option: any): boolean {
156160
}
157161
return false
158162
}
163+
164+
function genAssignmentCode (value: string, assignment: string): string {
165+
const modelRs = parseModel(value)
166+
if (modelRs.idx === null) {
167+
return `${value}=${assignment}`
168+
} else {
169+
return `var $$exp = ${modelRs.exp}, $$idx = ${modelRs.idx};` +
170+
`if (!Array.isArray($$exp)){` +
171+
`${value}=${assignment}}` +
172+
`else{$$exp.splice($$idx, 1, ${assignment})}`
173+
}
174+
}

src/platforms/web/util/model.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/* @flow */
2+
3+
let len, str, chr, index, expressionPos, expressionEndPos
4+
5+
/**
6+
* parse directive model to do the array update transform. a[idx] = val => $$a.splice($$idx, 1, val)
7+
*
8+
* for loop possible cases:
9+
*
10+
* - test
11+
* - test[idx]
12+
* - test[test1[idx]]
13+
* - test["a"][idx]
14+
* - xxx.test[a[a].test1[idx]]
15+
* - test.xxx.a["asa"][test1[idx]]
16+
*
17+
*/
18+
19+
export default function parseModel (val: string): Object {
20+
str = val
21+
len = str.length
22+
index = expressionPos = expressionEndPos = 0
23+
24+
if (val.indexOf('[') < 0) {
25+
return {
26+
exp: val,
27+
idx: null
28+
}
29+
}
30+
31+
while (!eof()) {
32+
chr = next()
33+
if (isStringStart(chr)) {
34+
parseString(chr)
35+
} else if (chr === 0x5B) {
36+
parseBracket(chr)
37+
}
38+
}
39+
40+
return {
41+
exp: val.substring(0, expressionPos),
42+
idx: val.substring(expressionPos + 1, expressionEndPos)
43+
}
44+
}
45+
46+
function next (): number {
47+
return str.charCodeAt(++index)
48+
}
49+
50+
function eof (): boolean {
51+
return index >= len
52+
}
53+
54+
function isStringStart (chr: number): boolean {
55+
return chr === 0x22 || chr === 0x27
56+
}
57+
58+
function parseBracket (chr: number): void {
59+
let inBracket = 1
60+
expressionPos = index
61+
while (!eof()) {
62+
chr = next()
63+
if (isStringStart(chr)) {
64+
parseString(chr)
65+
continue
66+
}
67+
if (chr === 0x5B) inBracket++
68+
if (chr === 0x5D) inBracket--
69+
if (inBracket === 0) {
70+
expressionEndPos = index
71+
break
72+
}
73+
}
74+
}
75+
76+
function parseString (chr: number): void {
77+
const stringQuote = chr
78+
while (!eof()) {
79+
chr = next()
80+
if (chr === stringQuote) {
81+
break
82+
}
83+
}
84+
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ import Vue from 'vue'
22

33
describe('Directive v-model component', () => {
44
it('should work', done => {
5+
const spy = jasmine.createSpy()
56
const vm = new Vue({
67
data: {
7-
msg: 'hello'
8+
msg: ['hello']
9+
},
10+
watch: {
11+
msg: spy
812
},
913
template: `
1014
<div>
1115
<p>{{ msg }}</p>
12-
<validate v-model="msg">
16+
<validate v-model="msg[0]">
1317
<input type="text">
1418
</validate>
1519
</div>
@@ -40,7 +44,8 @@ describe('Directive v-model component', () => {
4044
input.value = 'world'
4145
triggerEvent(input, 'input')
4246
}).then(() => {
43-
expect(vm.msg).toBe('world')
47+
expect(vm.msg).toEqual(['world'])
48+
expect(spy).toHaveBeenCalled()
4449
}).then(() => {
4550
document.body.removeChild(vm.$el)
4651
vm.$destroy()

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,46 @@ describe('Directive v-model radio', () => {
8585
}).then(done)
8686
})
8787

88+
it('multiple radios ', (done) => {
89+
const spy = jasmine.createSpy()
90+
const vm = new Vue({
91+
data: {
92+
selections: ['a', '1'],
93+
radioList: [
94+
{
95+
name: 'questionA',
96+
data: ['a', 'b', 'c']
97+
},
98+
{
99+
name: 'questionB',
100+
data: ['1', '2']
101+
}
102+
]
103+
},
104+
watch: {
105+
selections: spy
106+
},
107+
template:
108+
'<div>' +
109+
'<div v-for="(radioGroup, idx) in radioList">' +
110+
'<div>' +
111+
'<span v-for="(item, index) in radioGroup.data">' +
112+
'<input :name="radioGroup.name" type="radio" :value="item" v-model="selections[idx]" :id="idx"/>' +
113+
'<label>{{item}}</label>' +
114+
'</span>' +
115+
'</div>' +
116+
'</div>' +
117+
'</div>'
118+
}).$mount()
119+
document.body.appendChild(vm.$el)
120+
var inputs = vm.$el.getElementsByTagName('input')
121+
inputs[1].click()
122+
waitForUpdate(() => {
123+
expect(vm.selections).toEqual(['b', '1'])
124+
expect(spy).toHaveBeenCalled()
125+
}).then(done)
126+
})
127+
88128
it('warn inline checked', () => {
89129
const vm = new Vue({
90130
template: `<input v-model="test" type="radio" value="1" checked>`,

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,44 @@ describe('Directive v-model select', () => {
263263
}).then(done)
264264
})
265265

266+
it('multiple selects', (done) => {
267+
const spy = jasmine.createSpy()
268+
const vm = new Vue({
269+
data: {
270+
selections: ['', ''],
271+
selectBoxes: [
272+
[
273+
{ value: 'foo', text: 'foo' },
274+
{ value: 'bar', text: 'bar' }
275+
],
276+
[
277+
{ value: 'day', text: 'day' },
278+
{ value: 'night', text: 'night' }
279+
]
280+
]
281+
},
282+
watch: {
283+
selections: spy
284+
},
285+
template:
286+
'<div>' +
287+
'<select v-for="(item, index) in selectBoxes" v-model="selections[index]">' +
288+
'<option v-for="element in item" v-bind:value="element.value" v-text="element.text"></option>' +
289+
'</select>' +
290+
'<span ref="rs">{{selections}}</span>' +
291+
'</div>'
292+
}).$mount()
293+
document.body.appendChild(vm.$el)
294+
var selects = vm.$el.getElementsByTagName('select')
295+
var select0 = selects[0]
296+
select0.options[0].selected = true
297+
triggerEvent(select0, 'change')
298+
waitForUpdate(() => {
299+
expect(spy).toHaveBeenCalled()
300+
expect(vm.selections).toEqual(['foo', ''])
301+
}).then(done)
302+
})
303+
266304
it('should warn inline selected', () => {
267305
const vm = new Vue({
268306
data: {

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,47 @@ describe('Directive v-model text', () => {
6464
expect(vm.test).toBe('what')
6565
})
6666

67+
it('multiple inputs', (done) => {
68+
const spy = jasmine.createSpy()
69+
const vm = new Vue({
70+
data: {
71+
selections: [[1, 2, 3], [4, 5]],
72+
inputList: [
73+
{
74+
name: 'questionA',
75+
data: ['a', 'b', 'c']
76+
},
77+
{
78+
name: 'questionB',
79+
data: ['1', '2']
80+
}
81+
]
82+
},
83+
watch: {
84+
selections: spy
85+
},
86+
template:
87+
'<div>' +
88+
'<div v-for="(inputGroup, idx) in inputList">' +
89+
'<div>' +
90+
'<span v-for="(item, index) in inputGroup.data">' +
91+
'<input v-bind:name="item" type="text" v-model.number="selections[idx][index]" v-bind:id="idx+\'-\'+index"/>' +
92+
'<label>{{item}}</label>' +
93+
'</span>' +
94+
'</div>' +
95+
'</div>' +
96+
'<span ref="rs">{{selections}}</span>' +
97+
'</div>'
98+
}).$mount()
99+
var inputs = vm.$el.getElementsByTagName('input')
100+
inputs[1].value = 'test'
101+
triggerEvent(inputs[1], 'input')
102+
waitForUpdate(() => {
103+
expect(spy).toHaveBeenCalled()
104+
expect(vm.selections).toEqual([[1, 'test', 3], [4, 5]])
105+
}).then(done)
106+
})
107+
67108
if (isIE9) {
68109
it('IE9 selectionchange', done => {
69110
const vm = new Vue({

0 commit comments

Comments
 (0)