|
3 | 3 | type DirectiveHook,
|
4 | 4 | type ObjectDirective,
|
5 | 5 | type VNode,
|
| 6 | + nextTick, |
6 | 7 | warn,
|
7 | 8 | } from '@vue/runtime-core'
|
8 | 9 | import { addEventListener } from '../modules/events'
|
@@ -38,7 +39,9 @@ function onCompositionEnd(e: Event) {
|
38 | 39 |
|
39 | 40 | const assignKey = Symbol('_assign')
|
40 | 41 |
|
41 |
| -type ModelDirective<T> = ObjectDirective<T & { [assignKey]: AssignerFn }> |
| 42 | +type ModelDirective<T> = ObjectDirective< |
| 43 | + T & { [assignKey]: AssignerFn; _assigning?: boolean } |
| 44 | +> |
42 | 45 |
|
43 | 46 | // We are exporting the v-model runtime directly as vnode hooks so that it can
|
44 | 47 | // be tree-shaken in case v-model is never used.
|
@@ -197,38 +200,64 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
|
197 | 200 | : selectedVal
|
198 | 201 | : selectedVal[0],
|
199 | 202 | )
|
| 203 | + el._assigning = true |
| 204 | + nextTick(() => { |
| 205 | + el._assigning = false |
| 206 | + }) |
200 | 207 | })
|
201 | 208 | el[assignKey] = getModelAssigner(vnode)
|
202 | 209 | },
|
203 | 210 | // set value in mounted & updated because <select> relies on its children
|
204 | 211 | // <option>s.
|
205 |
| - mounted(el, { value }) { |
206 |
| - setSelected(el, value) |
| 212 | + mounted(el, { value, oldValue, modifiers: { number } }) { |
| 213 | + setSelected(el, value, oldValue, number) |
207 | 214 | },
|
208 | 215 | beforeUpdate(el, _binding, vnode) {
|
209 | 216 | el[assignKey] = getModelAssigner(vnode)
|
210 | 217 | },
|
211 |
| - updated(el, { value }) { |
212 |
| - setSelected(el, value) |
| 218 | + updated(el, { value, oldValue, modifiers: { number } }) { |
| 219 | + if (!el._assigning) { |
| 220 | + setSelected(el, value, oldValue, number) |
| 221 | + } |
213 | 222 | },
|
214 | 223 | }
|
215 | 224 |
|
216 |
| -function setSelected(el: HTMLSelectElement, value: any) { |
| 225 | +function setSelected( |
| 226 | + el: HTMLSelectElement, |
| 227 | + value: any, |
| 228 | + oldValue: any, |
| 229 | + number: boolean, |
| 230 | +) { |
217 | 231 | const isMultiple = el.multiple
|
218 |
| - if (isMultiple && !isArray(value) && !isSet(value)) { |
| 232 | + const isArrayValue = isArray(value) |
| 233 | + if (isMultiple && !isArrayValue && !isSet(value)) { |
219 | 234 | __DEV__ &&
|
220 | 235 | warn(
|
221 | 236 | `<select multiple v-model> expects an Array or Set value for its binding, ` +
|
222 | 237 | `but got ${Object.prototype.toString.call(value).slice(8, -1)}.`,
|
223 | 238 | )
|
224 | 239 | return
|
225 | 240 | }
|
| 241 | + |
| 242 | + // fast path for updates triggered by other changes |
| 243 | + if (isArrayValue && looseEqual(value, oldValue)) { |
| 244 | + return |
| 245 | + } |
| 246 | + |
226 | 247 | for (let i = 0, l = el.options.length; i < l; i++) {
|
227 | 248 | const option = el.options[i]
|
228 | 249 | const optionValue = getValue(option)
|
229 | 250 | if (isMultiple) {
|
230 |
| - if (isArray(value)) { |
231 |
| - option.selected = looseIndexOf(value, optionValue) > -1 |
| 251 | + if (isArrayValue) { |
| 252 | + const optionType = typeof optionValue |
| 253 | + // fast path for string / number values |
| 254 | + if (optionType === 'string' || optionType === 'number') { |
| 255 | + option.selected = value.includes( |
| 256 | + number ? looseToNumber(optionValue) : optionValue, |
| 257 | + ) |
| 258 | + } else { |
| 259 | + option.selected = looseIndexOf(value, optionValue) > -1 |
| 260 | + } |
232 | 261 | } else {
|
233 | 262 | option.selected = value.has(optionValue)
|
234 | 263 | }
|
|
0 commit comments