Bläddra i källkod

support using Objects as values in v-model select options (close #1115)

Evan You 10 år sedan
förälder
incheckning
59e2d1cf90
2 ändrade filer med 115 tillägg och 28 borttagningar
  1. 49 28
      src/directives/model/select.js
  2. 66 0
      test/unit/specs/directives/model_spec.js

+ 49 - 28
src/directives/model/select.js

@@ -25,9 +25,7 @@ module.exports = {
 
     // attach listener
     this.on('change', function () {
-      var value = self.multiple
-        ? getMultiValue(el)
-        : el.value
+      var value = getValue(el, self.multiple)
       value = self.number
         ? _.isArray(value)
           ? value.map(_.toNumber)
@@ -58,13 +56,16 @@ module.exports = {
     var multi = this.multiple && _.isArray(value)
     var options = el.options
     var i = options.length
-    var option
+    var op, val
     while (i--) {
-      option = options[i]
+      op = options[i]
+      val = op.hasOwnProperty('_value')
+        ? op._value
+        : op.value
       /* eslint-disable eqeqeq */
-      option.selected = multi
-        ? indexOf(value, option.value) > -1
-        : value == option.value
+      op.selected = multi
+        ? indexOf(value, val) > -1
+        : equals(value, val)
       /* eslint-enable eqeqeq */
     }
   },
@@ -139,10 +140,13 @@ function buildOptions (parent, options) {
       if (typeof op === 'string') {
         el.text = el.value = op
       } else {
-        if (op.value != null) {
+        if (op.value != null && !_.isObject(op.value)) {
           el.value = op.value
         }
-        el.text = op.text || op.value || ''
+        // object values gets serialized when set as value,
+        // so we store the raw value as a different property
+        el._value = op.value
+        el.text = op.text || ''
         if (op.disabled) {
           el.disabled = true
         }
@@ -181,29 +185,36 @@ function checkInitialValue () {
 }
 
 /**
- * Helper to extract a value array for select[multiple]
+ * Get select value
  *
  * @param {SelectElement} el
- * @return {Array}
+ * @param {Boolean} multi
+ * @return {Array|*}
  */
 
-function getMultiValue (el) {
-  return Array.prototype.filter
-    .call(el.options, filterSelected)
-    .map(getOptionValue)
-}
-
-function filterSelected (op) {
-  return op.selected
-}
-
-function getOptionValue (op) {
-  return op.value || op.text
+function getValue (el, multi) {
+  var i = el.options.length
+  var res = multi ? [] : null
+  var op, val
+  for (var i = 0, l = el.options.length; i < l; i++) {
+    op = el.options[i]
+    if (op.selected) {
+      val = op.hasOwnProperty('_value')
+        ? op._value
+        : op.value
+      if (multi) {
+        res.push(val)
+      } else {
+        return val
+      }
+    }
+  }
+  return res
 }
 
 /**
  * Native Array.indexOf uses strict equal, but in this
- * case we need to match string/numbers with soft equal.
+ * case we need to match string/numbers with custom equal.
  *
  * @param {Array} arr
  * @param {*} val
@@ -212,9 +223,19 @@ function getOptionValue (op) {
 function indexOf (arr, val) {
   var i = arr.length
   while (i--) {
-    /* eslint-disable eqeqeq */
-    if (arr[i] == val) return i
-    /* eslint-enable eqeqeq */
+    if (equals(arr[i], val)) {
+      return i
+    }
   }
   return -1
 }
+
+/**
+ * Check if two values are loosely equal. If two objects
+ * have the same shape, they are considered equal too:
+ *   equals({a: 1}, {a: 1}) => true
+ */
+
+function equals (a, b) {
+  return a == b || JSON.stringify(a) == JSON.stringify(b)
+}

+ 66 - 0
test/unit/specs/directives/model_spec.js

@@ -367,6 +367,72 @@ if (_.inBrowser) {
       expect(opts[2].selected).toBe(false)
     })
 
+    it('select + options with Object value', function (done) {
+      var vm = new Vue({
+        el: el,
+        data: {
+          test: { msg: 'A' },
+          opts: [
+            { text: 'a', value: { msg: 'A' }},
+            { text: 'b', value: { msg: 'B' }}
+          ]
+        },
+        template: '<select v-model="test" options="opts"></select>'
+      })
+      var select = el.firstChild
+      var opts = select.options
+      expect(opts[0].selected).toBe(true)
+      expect(opts[1].selected).toBe(false)
+      expect(vm.test.msg).toBe('A')
+      opts[1].selected = true
+      trigger(select, 'change')
+      _.nextTick(function () {
+        expect(opts[0].selected).toBe(false)
+        expect(opts[1].selected).toBe(true)
+        expect(vm.test.msg).toBe('B')
+        vm.test = { msg: 'A' }
+        _.nextTick(function () {
+          expect(opts[0].selected).toBe(true)
+          expect(opts[1].selected).toBe(false)
+          done()
+        })
+      })
+    })
+
+    it('select + options + multiple + Object value', function (done) {
+      var vm = new Vue({
+        el: el,
+        data: {
+          test: [{ msg: 'A' }, { msg: 'B'}],
+          opts: [
+            { text: 'a', value: { msg: 'A' }},
+            { text: 'b', value: { msg: 'B' }},
+            { text: 'c', value: { msg: 'C' }}
+          ]
+        },
+        template: '<select v-model="test" options="opts" multiple></select>'
+      })
+      var select = el.firstChild
+      var opts = select.options
+      expect(opts[0].selected).toBe(true)
+      expect(opts[1].selected).toBe(true)
+      expect(opts[2].selected).toBe(false)
+      vm.test = [{ msg: 'C' }]
+      _.nextTick(function () {
+        expect(opts[0].selected).toBe(false)
+        expect(opts[1].selected).toBe(false)
+        expect(opts[2].selected).toBe(true)
+        opts[1].selected = true
+        opts[2].selected = false
+        trigger(select, 'change')
+        _.nextTick(function () {
+          expect(vm.test.length).toBe(1)
+          expect(vm.test[0].msg).toBe('B')
+          done()
+        })
+      })
+    })
+
     it('select + number', function () {
       var vm = new Vue({
         el: el,