Skip to content

Commit

Permalink
support object looseEqual in v-model (fix #3673)
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Sep 14, 2016
1 parent d6a7568 commit 56960b5
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 12 deletions.
4 changes: 4 additions & 0 deletions flow/component.js
Expand Up @@ -89,6 +89,10 @@ declare interface Component {
_n: (value: string) => number | string;
// empty vnode
_e: () => VNode;
// loose equal
_q: (a: mixed, b: mixed) => boolean;
// loose indexOf
_i: (arr: Array<mixed>, val: mixed) => number;
// resolveFilter
_f: (id: string) => Function;
// renderList
Expand Down
6 changes: 5 additions & 1 deletion src/core/instance/render.js
Expand Up @@ -5,7 +5,7 @@ import VNode, { emptyVNode, cloneVNode, cloneVNodes } from '../vdom/vnode'
import { normalizeChildren } from '../vdom/helpers'
import {
warn, formatComponentName, bind, isObject, toObject,
nextTick, resolveAsset, _toString, toNumber
nextTick, resolveAsset, _toString, toNumber, looseEqual, looseIndexOf
} from '../util/index'

import { createElement } from '../vdom/create-element'
Expand Down Expand Up @@ -94,6 +94,10 @@ export function renderMixin (Vue: Class<Component>) {
Vue.prototype._n = toNumber
// empty vnode
Vue.prototype._e = emptyVNode
// loose equal
Vue.prototype._q = looseEqual
// loose indexOf
Vue.prototype._i = looseIndexOf

// render static tree by index
Vue.prototype._m = function renderStatic (
Expand Down
8 changes: 4 additions & 4 deletions src/platforms/web/compiler/directives/model.js
Expand Up @@ -40,16 +40,16 @@ function genCheckboxModel (el: ASTElement, value: string) {
const falseValueBinding = getBindingAttr(el, 'false-value') || 'false'
addProp(el, 'checked',
`Array.isArray(${value})` +
`?(${value}).indexOf(${valueBinding})>-1` +
`:(${value})===(${trueValueBinding})`
`?_i(${value},${valueBinding})>-1` +
`:_q(${value},${trueValueBinding})`
)
addHandler(el, 'change',
`var $$a=${value},` +
'$$el=$event.target,' +
`$$c=$$el.checked?(${trueValueBinding}):(${falseValueBinding});` +
'if(Array.isArray($$a)){' +
`var $$v=${valueBinding},` +
'$$i=$$a.indexOf($$v);' +
'$$i=_i($$a,$$v);' +
`if($$c){$$i<0&&(${value}=$$a.concat($$v))}` +
`else{$$i>-1&&(${value}=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}` +
`}else{${value}=$$c}`,
Expand All @@ -67,7 +67,7 @@ function genRadioModel (el: ASTElement, value: string) {
)
}
const valueBinding = getBindingAttr(el, 'value') || 'null'
addProp(el, 'checked', `(${value})===(${valueBinding})`)
addProp(el, 'checked', `_q(${value},${valueBinding})`)
addHandler(el, 'change', `${value}=${valueBinding}`, null, true)
}

Expand Down
7 changes: 4 additions & 3 deletions src/platforms/web/runtime/directives/model.js
Expand Up @@ -3,6 +3,7 @@
* properties to Elements.
*/

import { looseEqual, looseIndexOf } from 'shared/util'
import { warn } from 'core/util/index'
import { isAndroid, isIE9 } from 'web/util/index'

Expand Down Expand Up @@ -78,12 +79,12 @@ function setSelected (el, binding, vm) {
for (let i = 0, l = el.options.length; i < l; i++) {
option = el.options[i]
if (isMultiple) {
selected = value.indexOf(getValue(option)) > -1
selected = looseIndexOf(value, getValue(option)) > -1
if (option.selected !== selected) {
option.selected = selected
}
} else {
if (getValue(option) === value) {
if (looseEqual(getValue(option), value)) {
if (el.selectedIndex !== i) {
el.selectedIndex = i
}
Expand All @@ -98,7 +99,7 @@ function setSelected (el, binding, vm) {

function hasNoMatchingOption (value, options) {
for (let i = 0, l = options.length; i < l; i++) {
if (getValue(options[i]) === value) {
if (looseEqual(getValue(options[i]), value)) {
return false
}
}
Expand Down
23 changes: 22 additions & 1 deletion src/shared/util.js
Expand Up @@ -152,7 +152,7 @@ export function extend (to: Object, _from: ?Object): Object {
* Objects from primitive values when we know the value
* is a JSON-compliant type.
*/
export function isObject (obj: any): boolean {
export function isObject (obj: mixed): boolean {
return obj !== null && typeof obj === 'object'
}

Expand Down Expand Up @@ -197,3 +197,24 @@ export function genStaticKeys (modules: Array<ModuleOptions>): string {
return keys.concat(m.staticKeys || [])
}, []).join(',')
}

/**
* Check if two values are loosely equal - that is,
* if they are plain objects, do they have the same shape?
*/
export function looseEqual (a: mixed, b: mixed): boolean {
/* eslint-disable eqeqeq */
return a == b || (
isObject(a) && isObject(b)
? JSON.stringify(a) === JSON.stringify(b)
: false
)
/* eslint-enable eqeqeq */
}

export function looseIndexOf (arr: Array<mixed>, val: mixed): number {
for (let i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) return i
}
return -1
}
28 changes: 28 additions & 0 deletions test/unit/features/directives/model-checkbox.spec.js
Expand Up @@ -105,6 +105,34 @@ describe('Directive v-model checkbox', () => {
}).then(done)
})

it('bind to Array value with value bindings (object loose equal)', done => {
const vm = new Vue({
data: {
test: [{ a: 1 }]
},
template: `
<div>
<input type="checkbox" v-model="test" :value="{ a: 1 }">
<input type="checkbox" v-model="test" :value="{ a: 2 }">
</div>
`
}).$mount()
document.body.appendChild(vm.$el)
expect(vm.$el.children[0].checked).toBe(true)
expect(vm.$el.children[1].checked).toBe(false)
vm.$el.children[0].click()
expect(vm.test.length).toBe(0)
vm.$el.children[1].click()
expect(vm.test).toEqual([{ a: 2 }])
vm.$el.children[0].click()
expect(vm.test).toEqual([{ a: 2 }, { a: 1 }])
vm.test = [{ a: 1 }]
waitForUpdate(() => {
expect(vm.$el.children[0].checked).toBe(true)
expect(vm.$el.children[1].checked).toBe(false)
}).then(done)
})

it('warn inline checked', () => {
const vm = new Vue({
template: `<input type="checkbox" v-model="test" checked>`,
Expand Down
28 changes: 28 additions & 0 deletions test/unit/features/directives/model-radio.spec.js
Expand Up @@ -57,6 +57,34 @@ describe('Directive v-model radio', () => {
}).then(done)
})

it('should respect value bindings (object loose equal)', done => {
const vm = new Vue({
data: {
test: { a: 1 }
},
template: `
<div>
<input type="radio" :value="{ a: 1 }" v-model="test" name="test">
<input type="radio" :value="{ a: 2 }" v-model="test" name="test">
</div>
`
}).$mount()
document.body.appendChild(vm.$el)
expect(vm.$el.children[0].checked).toBe(true)
expect(vm.$el.children[1].checked).toBe(false)
vm.test = { a: 2 }
waitForUpdate(() => {
expect(vm.$el.children[0].checked).toBe(false)
expect(vm.$el.children[1].checked).toBe(true)
vm.$el.children[0].click()
expect(vm.$el.children[0].checked).toBe(true)
expect(vm.$el.children[1].checked).toBe(false)
expect(vm.test).toEqual({ a: 1 })
}).then(() => {
document.body.removeChild(vm.$el)
}).then(done)
})

it('warn inline checked', () => {
const vm = new Vue({
template: `<input v-model="test" type="radio" value="1" checked>`,
Expand Down
39 changes: 36 additions & 3 deletions test/unit/features/directives/model-select.spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue'
import { looseEqual } from 'shared/util'

/**
* setting <select>'s value in IE9 doesn't work
Expand All @@ -8,15 +9,19 @@ function updateSelect (el, value) {
var options = el.options
var i = options.length
while (i--) {
/* eslint-disable eqeqeq */
if (options[i].value == value) {
/* eslint-enable eqeqeq */
if (looseEqual(getValue(options[i]), value)) {
options[i].selected = true
break
}
}
}

function getValue (option) {
return '_value' in option
? option._value
: option.value || option.text
}

describe('Directive v-model select', () => {
it('should work', done => {
const vm = new Vue({
Expand Down Expand Up @@ -69,6 +74,34 @@ describe('Directive v-model select', () => {
}).then(done)
})

it('should work with value bindings (object loose equal)', done => {
const vm = new Vue({
data: {
test: { a: 2 }
},
template:
'<select v-model="test">' +
'<option value="1">a</option>' +
'<option :value="{ a: 2 }">b</option>' +
'<option :value="{ a: 3 }">c</option>' +
'</select>'
}).$mount()
document.body.appendChild(vm.$el)
expect(vm.$el.childNodes[1].selected).toBe(true)
vm.test = { a: 3 }
waitForUpdate(function () {
expect(vm.$el.childNodes[2].selected).toBe(true)

updateSelect(vm.$el, '1')
triggerEvent(vm.$el, 'change')
expect(vm.test).toBe('1')

updateSelect(vm.$el, { a: 2 })
triggerEvent(vm.$el, 'change')
expect(vm.test).toEqual({ a: 2 })
}).then(done)
})

it('should work with v-for', done => {
const vm = new Vue({
data: {
Expand Down

0 comments on commit 56960b5

Please sign in to comment.