select.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. var _ = require('../../util')
  2. var Watcher = require('../../watcher')
  3. var dirParser = require('../../parsers/directive')
  4. module.exports = {
  5. bind: function () {
  6. var self = this
  7. var el = this.el
  8. // method to force update DOM using latest value.
  9. this.forceUpdate = function () {
  10. if (self._watcher) {
  11. self.update(self._watcher.get())
  12. }
  13. }
  14. // check options param
  15. var optionsParam = this._checkParam('options')
  16. if (optionsParam) {
  17. initOptions.call(this, optionsParam)
  18. }
  19. this.number = this._checkParam('number') != null
  20. this.multiple = el.hasAttribute('multiple')
  21. // attach listener
  22. this.on('change', function () {
  23. var value = getValue(el, self.multiple)
  24. value = self.number
  25. ? _.isArray(value)
  26. ? value.map(_.toNumber)
  27. : _.toNumber(value)
  28. : value
  29. self.set(value)
  30. })
  31. // check initial value (inline selected attribute)
  32. checkInitialValue.call(this)
  33. // All major browsers except Firefox resets
  34. // selectedIndex with value -1 to 0 when the element
  35. // is appended to a new parent, therefore we have to
  36. // force a DOM update whenever that happens...
  37. this.vm.$on('hook:attached', this.forceUpdate)
  38. },
  39. update: function (value) {
  40. var el = this.el
  41. el.selectedIndex = -1
  42. if (value == null) {
  43. if (this.defaultOption) {
  44. this.defaultOption.selected = true
  45. }
  46. return
  47. }
  48. var multi = this.multiple && _.isArray(value)
  49. var options = el.options
  50. var i = options.length
  51. var op, val
  52. while (i--) {
  53. op = options[i]
  54. val = op.hasOwnProperty('_value')
  55. ? op._value
  56. : op.value
  57. /* eslint-disable eqeqeq */
  58. op.selected = multi
  59. ? indexOf(value, val) > -1
  60. : equals(value, val)
  61. /* eslint-enable eqeqeq */
  62. }
  63. },
  64. unbind: function () {
  65. this.vm.$off('hook:attached', this.forceUpdate)
  66. if (this.optionWatcher) {
  67. this.optionWatcher.teardown()
  68. }
  69. }
  70. }
  71. /**
  72. * Initialize the option list from the param.
  73. *
  74. * @param {String} expression
  75. */
  76. function initOptions (expression) {
  77. var self = this
  78. var el = self.el
  79. var defaultOption = self.defaultOption = self.el.options[0]
  80. var descriptor = dirParser.parse(expression)[0]
  81. function optionUpdateWatcher (value) {
  82. if (_.isArray(value)) {
  83. // clear old options.
  84. // cannot reset innerHTML here because IE family get
  85. // confused during compilation.
  86. var i = el.options.length
  87. while (i--) {
  88. var option = el.options[i]
  89. if (option !== defaultOption) {
  90. el.removeChild(option)
  91. }
  92. }
  93. buildOptions(el, value)
  94. self.forceUpdate()
  95. } else {
  96. process.env.NODE_ENV !== 'production' && _.warn(
  97. 'Invalid options value for v-model: ' + value
  98. )
  99. }
  100. }
  101. this.optionWatcher = new Watcher(
  102. this.vm,
  103. descriptor.expression,
  104. optionUpdateWatcher,
  105. {
  106. deep: true,
  107. filters: descriptor.filters
  108. }
  109. )
  110. // update with initial value
  111. optionUpdateWatcher(this.optionWatcher.value)
  112. }
  113. /**
  114. * Build up option elements. IE9 doesn't create options
  115. * when setting innerHTML on <select> elements, so we have
  116. * to use DOM API here.
  117. *
  118. * @param {Element} parent - a <select> or an <optgroup>
  119. * @param {Array} options
  120. */
  121. function buildOptions (parent, options) {
  122. var op, el
  123. for (var i = 0, l = options.length; i < l; i++) {
  124. op = options[i]
  125. if (!op.options) {
  126. el = document.createElement('option')
  127. if (typeof op === 'string') {
  128. el.text = el.value = op
  129. } else {
  130. if (op.value != null && !_.isObject(op.value)) {
  131. el.value = op.value
  132. }
  133. // object values gets serialized when set as value,
  134. // so we store the raw value as a different property
  135. el._value = op.value
  136. el.text = op.text || ''
  137. if (op.disabled) {
  138. el.disabled = true
  139. }
  140. }
  141. } else {
  142. el = document.createElement('optgroup')
  143. el.label = op.label
  144. buildOptions(el, op.options)
  145. }
  146. parent.appendChild(el)
  147. }
  148. }
  149. /**
  150. * Check the initial value for selected options.
  151. */
  152. function checkInitialValue () {
  153. var initValue
  154. var options = this.el.options
  155. for (var i = 0, l = options.length; i < l; i++) {
  156. if (options[i].hasAttribute('selected')) {
  157. if (this.multiple) {
  158. (initValue || (initValue = []))
  159. .push(options[i].value)
  160. } else {
  161. initValue = options[i].value
  162. }
  163. }
  164. }
  165. if (typeof initValue !== 'undefined') {
  166. this._initValue = this.number
  167. ? _.toNumber(initValue)
  168. : initValue
  169. }
  170. }
  171. /**
  172. * Get select value
  173. *
  174. * @param {SelectElement} el
  175. * @param {Boolean} multi
  176. * @return {Array|*}
  177. */
  178. function getValue (el, multi) {
  179. var i = el.options.length
  180. var res = multi ? [] : null
  181. var op, val
  182. for (var i = 0, l = el.options.length; i < l; i++) {
  183. op = el.options[i]
  184. if (op.selected) {
  185. val = op.hasOwnProperty('_value')
  186. ? op._value
  187. : op.value
  188. if (multi) {
  189. res.push(val)
  190. } else {
  191. return val
  192. }
  193. }
  194. }
  195. return res
  196. }
  197. /**
  198. * Native Array.indexOf uses strict equal, but in this
  199. * case we need to match string/numbers with custom equal.
  200. *
  201. * @param {Array} arr
  202. * @param {*} val
  203. */
  204. function indexOf (arr, val) {
  205. var i = arr.length
  206. while (i--) {
  207. if (equals(arr[i], val)) {
  208. return i
  209. }
  210. }
  211. return -1
  212. }
  213. /**
  214. * Check if two values are loosely equal. If two objects
  215. * have the same shape, they are considered equal too:
  216. * equals({a: 1}, {a: 1}) => true
  217. */
  218. function equals (a, b) {
  219. return a == b || JSON.stringify(a) == JSON.stringify(b)
  220. }