Skip to content

Commit 59e2d1c

Browse files
committed
support using Objects as values in v-model select options (close vuejs#1115)
1 parent b89e7c3 commit 59e2d1c

File tree

2 files changed

+115
-28
lines changed

2 files changed

+115
-28
lines changed

src/directives/model/select.js

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ module.exports = {
2525

2626
// attach listener
2727
this.on('change', function () {
28-
var value = self.multiple
29-
? getMultiValue(el)
30-
: el.value
28+
var value = getValue(el, self.multiple)
3129
value = self.number
3230
? _.isArray(value)
3331
? value.map(_.toNumber)
@@ -58,13 +56,16 @@ module.exports = {
5856
var multi = this.multiple && _.isArray(value)
5957
var options = el.options
6058
var i = options.length
61-
var option
59+
var op, val
6260
while (i--) {
63-
option = options[i]
61+
op = options[i]
62+
val = op.hasOwnProperty('_value')
63+
? op._value
64+
: op.value
6465
/* eslint-disable eqeqeq */
65-
option.selected = multi
66-
? indexOf(value, option.value) > -1
67-
: value == option.value
66+
op.selected = multi
67+
? indexOf(value, val) > -1
68+
: equals(value, val)
6869
/* eslint-enable eqeqeq */
6970
}
7071
},
@@ -139,10 +140,13 @@ function buildOptions (parent, options) {
139140
if (typeof op === 'string') {
140141
el.text = el.value = op
141142
} else {
142-
if (op.value != null) {
143+
if (op.value != null && !_.isObject(op.value)) {
143144
el.value = op.value
144145
}
145-
el.text = op.text || op.value || ''
146+
// object values gets serialized when set as value,
147+
// so we store the raw value as a different property
148+
el._value = op.value
149+
el.text = op.text || ''
146150
if (op.disabled) {
147151
el.disabled = true
148152
}
@@ -181,29 +185,36 @@ function checkInitialValue () {
181185
}
182186

183187
/**
184-
* Helper to extract a value array for select[multiple]
188+
* Get select value
185189
*
186190
* @param {SelectElement} el
187-
* @return {Array}
191+
* @param {Boolean} multi
192+
* @return {Array|*}
188193
*/
189194

190-
function getMultiValue (el) {
191-
return Array.prototype.filter
192-
.call(el.options, filterSelected)
193-
.map(getOptionValue)
194-
}
195-
196-
function filterSelected (op) {
197-
return op.selected
198-
}
199-
200-
function getOptionValue (op) {
201-
return op.value || op.text
195+
function getValue (el, multi) {
196+
var i = el.options.length
197+
var res = multi ? [] : null
198+
var op, val
199+
for (var i = 0, l = el.options.length; i < l; i++) {
200+
op = el.options[i]
201+
if (op.selected) {
202+
val = op.hasOwnProperty('_value')
203+
? op._value
204+
: op.value
205+
if (multi) {
206+
res.push(val)
207+
} else {
208+
return val
209+
}
210+
}
211+
}
212+
return res
202213
}
203214

204215
/**
205216
* Native Array.indexOf uses strict equal, but in this
206-
* case we need to match string/numbers with soft equal.
217+
* case we need to match string/numbers with custom equal.
207218
*
208219
* @param {Array} arr
209220
* @param {*} val
@@ -212,9 +223,19 @@ function getOptionValue (op) {
212223
function indexOf (arr, val) {
213224
var i = arr.length
214225
while (i--) {
215-
/* eslint-disable eqeqeq */
216-
if (arr[i] == val) return i
217-
/* eslint-enable eqeqeq */
226+
if (equals(arr[i], val)) {
227+
return i
228+
}
218229
}
219230
return -1
220231
}
232+
233+
/**
234+
* Check if two values are loosely equal. If two objects
235+
* have the same shape, they are considered equal too:
236+
* equals({a: 1}, {a: 1}) => true
237+
*/
238+
239+
function equals (a, b) {
240+
return a == b || JSON.stringify(a) == JSON.stringify(b)
241+
}

test/unit/specs/directives/model_spec.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,72 @@ if (_.inBrowser) {
367367
expect(opts[2].selected).toBe(false)
368368
})
369369

370+
it('select + options with Object value', function (done) {
371+
var vm = new Vue({
372+
el: el,
373+
data: {
374+
test: { msg: 'A' },
375+
opts: [
376+
{ text: 'a', value: { msg: 'A' }},
377+
{ text: 'b', value: { msg: 'B' }}
378+
]
379+
},
380+
template: '<select v-model="test" options="opts"></select>'
381+
})
382+
var select = el.firstChild
383+
var opts = select.options
384+
expect(opts[0].selected).toBe(true)
385+
expect(opts[1].selected).toBe(false)
386+
expect(vm.test.msg).toBe('A')
387+
opts[1].selected = true
388+
trigger(select, 'change')
389+
_.nextTick(function () {
390+
expect(opts[0].selected).toBe(false)
391+
expect(opts[1].selected).toBe(true)
392+
expect(vm.test.msg).toBe('B')
393+
vm.test = { msg: 'A' }
394+
_.nextTick(function () {
395+
expect(opts[0].selected).toBe(true)
396+
expect(opts[1].selected).toBe(false)
397+
done()
398+
})
399+
})
400+
})
401+
402+
it('select + options + multiple + Object value', function (done) {
403+
var vm = new Vue({
404+
el: el,
405+
data: {
406+
test: [{ msg: 'A' }, { msg: 'B'}],
407+
opts: [
408+
{ text: 'a', value: { msg: 'A' }},
409+
{ text: 'b', value: { msg: 'B' }},
410+
{ text: 'c', value: { msg: 'C' }}
411+
]
412+
},
413+
template: '<select v-model="test" options="opts" multiple></select>'
414+
})
415+
var select = el.firstChild
416+
var opts = select.options
417+
expect(opts[0].selected).toBe(true)
418+
expect(opts[1].selected).toBe(true)
419+
expect(opts[2].selected).toBe(false)
420+
vm.test = [{ msg: 'C' }]
421+
_.nextTick(function () {
422+
expect(opts[0].selected).toBe(false)
423+
expect(opts[1].selected).toBe(false)
424+
expect(opts[2].selected).toBe(true)
425+
opts[1].selected = true
426+
opts[2].selected = false
427+
trigger(select, 'change')
428+
_.nextTick(function () {
429+
expect(vm.test.length).toBe(1)
430+
expect(vm.test[0].msg).toBe('B')
431+
done()
432+
})
433+
})
434+
})
435+
370436
it('select + number', function () {
371437
var vm = new Vue({
372438
el: el,

0 commit comments

Comments
 (0)