|
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