feat(runtime-vapor): support v-model w/ select tag

This commit is contained in:
三咲智子 Kevin Deng 2024-02-04 22:27:24 +08:00
parent cde91e4fb5
commit ed954bcd33
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
2 changed files with 127 additions and 8 deletions

View File

@ -36,6 +36,7 @@ export type DirectiveHookName =
export type ObjectDirective<T = any, V = any, M extends string = string> = {
[K in DirectiveHookName]?: DirectiveHook<T, V, M> | undefined
} & {
/** Watch value deeply */
deep?: boolean
}
@ -100,7 +101,7 @@ export function withDirectives<T extends Node>(
// register source
if (source) {
// callback will be overridden by middleware
renderWatch(source, NOOP)
renderWatch(source, NOOP, { deep: dir.deep })
}
}

View File

@ -1,7 +1,16 @@
import {
invokeArrayFns,
isArray,
isSet,
looseEqual,
looseIndexOf,
looseToNumber,
} from '@vue/shared'
import type { ComponentInternalInstance } from '../component'
import type { ObjectDirective } from '../directive'
import { on } from '../dom/on'
import { invokeArrayFns, isArray, looseToNumber } from '@vue/shared'
import { nextTick } from '../scheduler'
import { warn } from '../warning'
type AssignerFn = (value: any) => void
@ -26,16 +35,19 @@ function onCompositionEnd(e: Event) {
}
}
const assignKeyMap = new WeakMap<HTMLElement, AssignerFn>()
const assignFnMap = new WeakMap<HTMLElement, AssignerFn>()
const assigningMap = new WeakMap<HTMLElement, boolean>()
// We are exporting the v-model runtime directly as vnode hooks so that it can
// be tree-shaken in case v-model is never used.
export const vModelText: ObjectDirective<
HTMLInputElement | HTMLTextAreaElement
HTMLInputElement | HTMLTextAreaElement,
any,
'lazy' | 'trim' | 'number'
> = {
beforeMount(el, { instance, modifiers: { lazy, trim, number } = {} }) {
const assigner = getModelAssigner(el, instance)
assignKeyMap.set(el, assigner)
assignFnMap.set(el, assigner)
const castToNumber = number // || (vnode.props && vnode.props.type === 'number')
on(el, lazy ? 'change' : 'input', e => {
@ -72,7 +84,7 @@ export const vModelText: ObjectDirective<
el,
{ instance, value, modifiers: { lazy, trim, number } = {} },
) {
assignKeyMap.set(el, getModelAssigner(el, instance))
assignFnMap.set(el, getModelAssigner(el, instance))
// avoid clearing unresolved text. #2302
if ((el as any).composing) return
@ -100,7 +112,113 @@ export const vModelText: ObjectDirective<
}
// TODO
export const vModelDynamic = {}
export const vModelRadio = {}
export const vModelSelect: ObjectDirective<HTMLSelectElement, any, 'number'> = {
// <select multiple> value need to be deep traversed
deep: true,
beforeMount(
el,
{ value, oldValue, instance, modifiers: { number = false } = {} },
) {
const isSetModel = isSet(value)
on(el, 'change', () => {
const selectedVal = Array.prototype.filter
.call(el.options, (o: HTMLOptionElement) => o.selected)
.map((o: HTMLOptionElement) =>
number ? looseToNumber(getValue(o, instance)) : getValue(o, instance),
)
assignFnMap.get(el)!(
el.multiple
? isSetModel
? new Set(selectedVal)
: selectedVal
: selectedVal[0],
)
assigningMap.set(el, true)
nextTick(() => {
assigningMap.set(el, false)
})
})
assignFnMap.set(el, getModelAssigner(el, instance))
setSelected(el, instance, value, oldValue, number)
},
beforeUpdate(el, { instance }) {
assignFnMap.set(el, getModelAssigner(el, instance))
},
updated(
el,
{ value, oldValue, instance, modifiers: { number = false } = {} },
) {
if (!assigningMap.get(el)) {
setSelected(el, instance, value, oldValue, number)
}
},
}
function setSelected(
el: HTMLSelectElement,
instance: ComponentInternalInstance,
value: any,
oldValue: any,
number: boolean,
) {
const isMultiple = el.multiple
const isArrayValue = isArray(value)
if (isMultiple && !isArrayValue && !isSet(value)) {
__DEV__ &&
warn(
`<select multiple v-model> expects an Array or Set value for its binding, ` +
`but got ${Object.prototype.toString.call(value).slice(8, -1)}.`,
)
return
}
// Disable fast path due to https://github.com/vuejs/core/issues/10267
// fast path for updates triggered by other changes
// if (isArrayValue && looseEqual(value, oldValue)) {
// return
// }
for (let i = 0, l = el.options.length; i < l; i++) {
const option = el.options[i]
const optionValue = getValue(option, instance)
if (isMultiple) {
if (isArrayValue) {
const optionType = typeof optionValue
// fast path for string / number values
if (optionType === 'string' || optionType === 'number') {
option.selected = value.includes(
number ? looseToNumber(optionValue) : optionValue,
)
} else {
option.selected = looseIndexOf(value, optionValue) > -1
}
} else {
option.selected = value.has(optionValue)
}
} else {
if (looseEqual(getValue(option, instance), value)) {
if (el.selectedIndex !== i) el.selectedIndex = i
return
}
}
}
if (!isMultiple && el.selectedIndex !== -1) {
el.selectedIndex = -1
}
}
// retrieve raw value set via :value bindings
function getValue(
el: HTMLOptionElement | HTMLInputElement,
instance: ComponentInternalInstance,
) {
const metadata = instance.metadata.get(el)
return metadata ? metadata.props.value : el.value
}
export const vModelCheckbox = {}
export const vModelSelect = {}
export const vModelDynamic = {}