Skip to content

Commit 56960b5

Browse files
committed
support object looseEqual in v-model (fix vuejs#3673)
1 parent d6a7568 commit 56960b5

File tree

8 files changed

+131
-12
lines changed

8 files changed

+131
-12
lines changed

flow/component.js

+4
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ declare interface Component {
8989
_n: (value: string) => number | string;
9090
// empty vnode
9191
_e: () => VNode;
92+
// loose equal
93+
_q: (a: mixed, b: mixed) => boolean;
94+
// loose indexOf
95+
_i: (arr: Array<mixed>, val: mixed) => number;
9296
// resolveFilter
9397
_f: (id: string) => Function;
9498
// renderList

src/core/instance/render.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import VNode, { emptyVNode, cloneVNode, cloneVNodes } from '../vdom/vnode'
55
import { normalizeChildren } from '../vdom/helpers'
66
import {
77
warn, formatComponentName, bind, isObject, toObject,
8-
nextTick, resolveAsset, _toString, toNumber
8+
nextTick, resolveAsset, _toString, toNumber, looseEqual, looseIndexOf
99
} from '../util/index'
1010

1111
import { createElement } from '../vdom/create-element'
@@ -94,6 +94,10 @@ export function renderMixin (Vue: Class<Component>) {
9494
Vue.prototype._n = toNumber
9595
// empty vnode
9696
Vue.prototype._e = emptyVNode
97+
// loose equal
98+
Vue.prototype._q = looseEqual
99+
// loose indexOf
100+
Vue.prototype._i = looseIndexOf
97101

98102
// render static tree by index
99103
Vue.prototype._m = function renderStatic (

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ function genCheckboxModel (el: ASTElement, value: string) {
4040
const falseValueBinding = getBindingAttr(el, 'false-value') || 'false'
4141
addProp(el, 'checked',
4242
`Array.isArray(${value})` +
43-
`?(${value}).indexOf(${valueBinding})>-1` +
44-
`:(${value})===(${trueValueBinding})`
43+
`?_i(${value},${valueBinding})>-1` +
44+
`:_q(${value},${trueValueBinding})`
4545
)
4646
addHandler(el, 'change',
4747
`var $$a=${value},` +
4848
'$$el=$event.target,' +
4949
`$$c=$$el.checked?(${trueValueBinding}):(${falseValueBinding});` +
5050
'if(Array.isArray($$a)){' +
5151
`var $$v=${valueBinding},` +
52-
'$$i=$$a.indexOf($$v);' +
52+
'$$i=_i($$a,$$v);' +
5353
`if($$c){$$i<0&&(${value}=$$a.concat($$v))}` +
5454
`else{$$i>-1&&(${value}=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}` +
5555
`}else{${value}=$$c}`,
@@ -67,7 +67,7 @@ function genRadioModel (el: ASTElement, value: string) {
6767
)
6868
}
6969
const valueBinding = getBindingAttr(el, 'value') || 'null'
70-
addProp(el, 'checked', `(${value})===(${valueBinding})`)
70+
addProp(el, 'checked', `_q(${value},${valueBinding})`)
7171
addHandler(el, 'change', `${value}=${valueBinding}`, null, true)
7272
}
7373

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* properties to Elements.
44
*/
55

6+
import { looseEqual, looseIndexOf } from 'shared/util'
67
import { warn } from 'core/util/index'
78
import { isAndroid, isIE9 } from 'web/util/index'
89

@@ -78,12 +79,12 @@ function setSelected (el, binding, vm) {
7879
for (let i = 0, l = el.options.length; i < l; i++) {
7980
option = el.options[i]
8081
if (isMultiple) {
81-
selected = value.indexOf(getValue(option)) > -1
82+
selected = looseIndexOf(value, getValue(option)) > -1
8283
if (option.selected !== selected) {
8384
option.selected = selected
8485
}
8586
} else {
86-
if (getValue(option) === value) {
87+
if (looseEqual(getValue(option), value)) {
8788
if (el.selectedIndex !== i) {
8889
el.selectedIndex = i
8990
}
@@ -98,7 +99,7 @@ function setSelected (el, binding, vm) {
9899

99100
function hasNoMatchingOption (value, options) {
100101
for (let i = 0, l = options.length; i < l; i++) {
101-
if (getValue(options[i]) === value) {
102+
if (looseEqual(getValue(options[i]), value)) {
102103
return false
103104
}
104105
}

src/shared/util.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export function extend (to: Object, _from: ?Object): Object {
152152
* Objects from primitive values when we know the value
153153
* is a JSON-compliant type.
154154
*/
155-
export function isObject (obj: any): boolean {
155+
export function isObject (obj: mixed): boolean {
156156
return obj !== null && typeof obj === 'object'
157157
}
158158

@@ -197,3 +197,24 @@ export function genStaticKeys (modules: Array<ModuleOptions>): string {
197197
return keys.concat(m.staticKeys || [])
198198
}, []).join(',')
199199
}
200+
201+
/**
202+
* Check if two values are loosely equal - that is,
203+
* if they are plain objects, do they have the same shape?
204+
*/
205+
export function looseEqual (a: mixed, b: mixed): boolean {
206+
/* eslint-disable eqeqeq */
207+
return a == b || (
208+
isObject(a) && isObject(b)
209+
? JSON.stringify(a) === JSON.stringify(b)
210+
: false
211+
)
212+
/* eslint-enable eqeqeq */
213+
}
214+
215+
export function looseIndexOf (arr: Array<mixed>, val: mixed): number {
216+
for (let i = 0; i < arr.length; i++) {
217+
if (looseEqual(arr[i], val)) return i
218+
}
219+
return -1
220+
}

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

+28
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,34 @@ describe('Directive v-model checkbox', () => {
105105
}).then(done)
106106
})
107107

108+
it('bind to Array value with value bindings (object loose equal)', done => {
109+
const vm = new Vue({
110+
data: {
111+
test: [{ a: 1 }]
112+
},
113+
template: `
114+
<div>
115+
<input type="checkbox" v-model="test" :value="{ a: 1 }">
116+
<input type="checkbox" v-model="test" :value="{ a: 2 }">
117+
</div>
118+
`
119+
}).$mount()
120+
document.body.appendChild(vm.$el)
121+
expect(vm.$el.children[0].checked).toBe(true)
122+
expect(vm.$el.children[1].checked).toBe(false)
123+
vm.$el.children[0].click()
124+
expect(vm.test.length).toBe(0)
125+
vm.$el.children[1].click()
126+
expect(vm.test).toEqual([{ a: 2 }])
127+
vm.$el.children[0].click()
128+
expect(vm.test).toEqual([{ a: 2 }, { a: 1 }])
129+
vm.test = [{ a: 1 }]
130+
waitForUpdate(() => {
131+
expect(vm.$el.children[0].checked).toBe(true)
132+
expect(vm.$el.children[1].checked).toBe(false)
133+
}).then(done)
134+
})
135+
108136
it('warn inline checked', () => {
109137
const vm = new Vue({
110138
template: `<input type="checkbox" v-model="test" checked>`,

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

+28
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,34 @@ describe('Directive v-model radio', () => {
5757
}).then(done)
5858
})
5959

60+
it('should respect value bindings (object loose equal)', done => {
61+
const vm = new Vue({
62+
data: {
63+
test: { a: 1 }
64+
},
65+
template: `
66+
<div>
67+
<input type="radio" :value="{ a: 1 }" v-model="test" name="test">
68+
<input type="radio" :value="{ a: 2 }" v-model="test" name="test">
69+
</div>
70+
`
71+
}).$mount()
72+
document.body.appendChild(vm.$el)
73+
expect(vm.$el.children[0].checked).toBe(true)
74+
expect(vm.$el.children[1].checked).toBe(false)
75+
vm.test = { a: 2 }
76+
waitForUpdate(() => {
77+
expect(vm.$el.children[0].checked).toBe(false)
78+
expect(vm.$el.children[1].checked).toBe(true)
79+
vm.$el.children[0].click()
80+
expect(vm.$el.children[0].checked).toBe(true)
81+
expect(vm.$el.children[1].checked).toBe(false)
82+
expect(vm.test).toEqual({ a: 1 })
83+
}).then(() => {
84+
document.body.removeChild(vm.$el)
85+
}).then(done)
86+
})
87+
6088
it('warn inline checked', () => {
6189
const vm = new Vue({
6290
template: `<input v-model="test" type="radio" value="1" checked>`,

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

+36-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Vue from 'vue'
2+
import { looseEqual } from 'shared/util'
23

34
/**
45
* setting <select>'s value in IE9 doesn't work
@@ -8,15 +9,19 @@ function updateSelect (el, value) {
89
var options = el.options
910
var i = options.length
1011
while (i--) {
11-
/* eslint-disable eqeqeq */
12-
if (options[i].value == value) {
13-
/* eslint-enable eqeqeq */
12+
if (looseEqual(getValue(options[i]), value)) {
1413
options[i].selected = true
1514
break
1615
}
1716
}
1817
}
1918

19+
function getValue (option) {
20+
return '_value' in option
21+
? option._value
22+
: option.value || option.text
23+
}
24+
2025
describe('Directive v-model select', () => {
2126
it('should work', done => {
2227
const vm = new Vue({
@@ -69,6 +74,34 @@ describe('Directive v-model select', () => {
6974
}).then(done)
7075
})
7176

77+
it('should work with value bindings (object loose equal)', done => {
78+
const vm = new Vue({
79+
data: {
80+
test: { a: 2 }
81+
},
82+
template:
83+
'<select v-model="test">' +
84+
'<option value="1">a</option>' +
85+
'<option :value="{ a: 2 }">b</option>' +
86+
'<option :value="{ a: 3 }">c</option>' +
87+
'</select>'
88+
}).$mount()
89+
document.body.appendChild(vm.$el)
90+
expect(vm.$el.childNodes[1].selected).toBe(true)
91+
vm.test = { a: 3 }
92+
waitForUpdate(function () {
93+
expect(vm.$el.childNodes[2].selected).toBe(true)
94+
95+
updateSelect(vm.$el, '1')
96+
triggerEvent(vm.$el, 'change')
97+
expect(vm.test).toBe('1')
98+
99+
updateSelect(vm.$el, { a: 2 })
100+
triggerEvent(vm.$el, 'change')
101+
expect(vm.test).toEqual({ a: 2 })
102+
}).then(done)
103+
})
104+
72105
it('should work with v-for', done => {
73106
const vm = new Vue({
74107
data: {

0 commit comments

Comments
 (0)