Pārlūkot izejas kodu

support object looseEqual in v-model (fix #3673)

Evan You 9 gadi atpakaļ
vecāks
revīzija
56960b5fbc

+ 4 - 0
flow/component.js

@@ -89,6 +89,10 @@ declare interface Component {
   _n: (value: string) => number | string;
   _n: (value: string) => number | string;
   // empty vnode
   // empty vnode
   _e: () => VNode;
   _e: () => VNode;
+  // loose equal
+  _q: (a: mixed, b: mixed) => boolean;
+  // loose indexOf
+  _i: (arr: Array<mixed>, val: mixed) => number;
   // resolveFilter
   // resolveFilter
   _f: (id: string) => Function;
   _f: (id: string) => Function;
   // renderList
   // renderList

+ 5 - 1
src/core/instance/render.js

@@ -5,7 +5,7 @@ import VNode, { emptyVNode, cloneVNode, cloneVNodes } from '../vdom/vnode'
 import { normalizeChildren } from '../vdom/helpers'
 import { normalizeChildren } from '../vdom/helpers'
 import {
 import {
   warn, formatComponentName, bind, isObject, toObject,
   warn, formatComponentName, bind, isObject, toObject,
-  nextTick, resolveAsset, _toString, toNumber
+  nextTick, resolveAsset, _toString, toNumber, looseEqual, looseIndexOf
 } from '../util/index'
 } from '../util/index'
 
 
 import { createElement } from '../vdom/create-element'
 import { createElement } from '../vdom/create-element'
@@ -94,6 +94,10 @@ export function renderMixin (Vue: Class<Component>) {
   Vue.prototype._n = toNumber
   Vue.prototype._n = toNumber
   // empty vnode
   // empty vnode
   Vue.prototype._e = emptyVNode
   Vue.prototype._e = emptyVNode
+  // loose equal
+  Vue.prototype._q = looseEqual
+  // loose indexOf
+  Vue.prototype._i = looseIndexOf
 
 
   // render static tree by index
   // render static tree by index
   Vue.prototype._m = function renderStatic (
   Vue.prototype._m = function renderStatic (

+ 4 - 4
src/platforms/web/compiler/directives/model.js

@@ -40,8 +40,8 @@ function genCheckboxModel (el: ASTElement, value: string) {
   const falseValueBinding = getBindingAttr(el, 'false-value') || 'false'
   const falseValueBinding = getBindingAttr(el, 'false-value') || 'false'
   addProp(el, 'checked',
   addProp(el, 'checked',
     `Array.isArray(${value})` +
     `Array.isArray(${value})` +
-      `?(${value}).indexOf(${valueBinding})>-1` +
-      `:(${value})===(${trueValueBinding})`
+      `?_i(${value},${valueBinding})>-1` +
+      `:_q(${value},${trueValueBinding})`
   )
   )
   addHandler(el, 'change',
   addHandler(el, 'change',
     `var $$a=${value},` +
     `var $$a=${value},` +
@@ -49,7 +49,7 @@ function genCheckboxModel (el: ASTElement, value: string) {
         `$$c=$$el.checked?(${trueValueBinding}):(${falseValueBinding});` +
         `$$c=$$el.checked?(${trueValueBinding}):(${falseValueBinding});` +
     'if(Array.isArray($$a)){' +
     'if(Array.isArray($$a)){' +
       `var $$v=${valueBinding},` +
       `var $$v=${valueBinding},` +
-          '$$i=$$a.indexOf($$v);' +
+          '$$i=_i($$a,$$v);' +
       `if($$c){$$i<0&&(${value}=$$a.concat($$v))}` +
       `if($$c){$$i<0&&(${value}=$$a.concat($$v))}` +
       `else{$$i>-1&&(${value}=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}` +
       `else{$$i>-1&&(${value}=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}` +
     `}else{${value}=$$c}`,
     `}else{${value}=$$c}`,
@@ -67,7 +67,7 @@ function genRadioModel (el: ASTElement, value: string) {
     )
     )
   }
   }
   const valueBinding = getBindingAttr(el, 'value') || 'null'
   const valueBinding = getBindingAttr(el, 'value') || 'null'
-  addProp(el, 'checked', `(${value})===(${valueBinding})`)
+  addProp(el, 'checked', `_q(${value},${valueBinding})`)
   addHandler(el, 'change', `${value}=${valueBinding}`, null, true)
   addHandler(el, 'change', `${value}=${valueBinding}`, null, true)
 }
 }
 
 

+ 4 - 3
src/platforms/web/runtime/directives/model.js

@@ -3,6 +3,7 @@
  * properties to Elements.
  * properties to Elements.
  */
  */
 
 
+import { looseEqual, looseIndexOf } from 'shared/util'
 import { warn } from 'core/util/index'
 import { warn } from 'core/util/index'
 import { isAndroid, isIE9 } from 'web/util/index'
 import { isAndroid, isIE9 } from 'web/util/index'
 
 
@@ -78,12 +79,12 @@ function setSelected (el, binding, vm) {
   for (let i = 0, l = el.options.length; i < l; i++) {
   for (let i = 0, l = el.options.length; i < l; i++) {
     option = el.options[i]
     option = el.options[i]
     if (isMultiple) {
     if (isMultiple) {
-      selected = value.indexOf(getValue(option)) > -1
+      selected = looseIndexOf(value, getValue(option)) > -1
       if (option.selected !== selected) {
       if (option.selected !== selected) {
         option.selected = selected
         option.selected = selected
       }
       }
     } else {
     } else {
-      if (getValue(option) === value) {
+      if (looseEqual(getValue(option), value)) {
         if (el.selectedIndex !== i) {
         if (el.selectedIndex !== i) {
           el.selectedIndex = i
           el.selectedIndex = i
         }
         }
@@ -98,7 +99,7 @@ function setSelected (el, binding, vm) {
 
 
 function hasNoMatchingOption (value, options) {
 function hasNoMatchingOption (value, options) {
   for (let i = 0, l = options.length; i < l; i++) {
   for (let i = 0, l = options.length; i < l; i++) {
-    if (getValue(options[i]) === value) {
+    if (looseEqual(getValue(options[i]), value)) {
       return false
       return false
     }
     }
   }
   }

+ 22 - 1
src/shared/util.js

@@ -152,7 +152,7 @@ export function extend (to: Object, _from: ?Object): Object {
  * Objects from primitive values when we know the value
  * Objects from primitive values when we know the value
  * is a JSON-compliant type.
  * is a JSON-compliant type.
  */
  */
-export function isObject (obj: any): boolean {
+export function isObject (obj: mixed): boolean {
   return obj !== null && typeof obj === 'object'
   return obj !== null && typeof obj === 'object'
 }
 }
 
 
@@ -197,3 +197,24 @@ export function genStaticKeys (modules: Array<ModuleOptions>): string {
     return keys.concat(m.staticKeys || [])
     return keys.concat(m.staticKeys || [])
   }, []).join(',')
   }, []).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 - 0
test/unit/features/directives/model-checkbox.spec.js

@@ -105,6 +105,34 @@ describe('Directive v-model checkbox', () => {
     }).then(done)
     }).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', () => {
   it('warn inline checked', () => {
     const vm = new Vue({
     const vm = new Vue({
       template: `<input type="checkbox" v-model="test" checked>`,
       template: `<input type="checkbox" v-model="test" checked>`,

+ 28 - 0
test/unit/features/directives/model-radio.spec.js

@@ -57,6 +57,34 @@ describe('Directive v-model radio', () => {
     }).then(done)
     }).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', () => {
   it('warn inline checked', () => {
     const vm = new Vue({
     const vm = new Vue({
       template: `<input v-model="test" type="radio" value="1" checked>`,
       template: `<input v-model="test" type="radio" value="1" checked>`,

+ 36 - 3
test/unit/features/directives/model-select.spec.js

@@ -1,4 +1,5 @@
 import Vue from 'vue'
 import Vue from 'vue'
+import { looseEqual } from 'shared/util'
 
 
 /**
 /**
  * setting <select>'s value in IE9 doesn't work
  * setting <select>'s value in IE9 doesn't work
@@ -8,15 +9,19 @@ function updateSelect (el, value) {
   var options = el.options
   var options = el.options
   var i = options.length
   var i = options.length
   while (i--) {
   while (i--) {
-    /* eslint-disable eqeqeq */
-    if (options[i].value == value) {
-    /* eslint-enable eqeqeq */
+    if (looseEqual(getValue(options[i]), value)) {
       options[i].selected = true
       options[i].selected = true
       break
       break
     }
     }
   }
   }
 }
 }
 
 
+function getValue (option) {
+  return '_value' in option
+    ? option._value
+    : option.value || option.text
+}
+
 describe('Directive v-model select', () => {
 describe('Directive v-model select', () => {
   it('should work', done => {
   it('should work', done => {
     const vm = new Vue({
     const vm = new Vue({
@@ -69,6 +74,34 @@ describe('Directive v-model select', () => {
     }).then(done)
     }).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 => {
   it('should work with v-for', done => {
     const vm = new Vue({
     const vm = new Vue({
       data: {
       data: {