feat(runtime-dom): allow native Set as v-model checkbox source (#1957)

This commit is contained in:
Pick 2020-09-14 23:16:50 +08:00 committed by GitHub
parent 542680e478
commit cf1b6c666f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 104 additions and 4 deletions

View File

@ -433,6 +433,76 @@ describe('vModel', () => {
expect(bar.checked).toEqual(false) expect(bar.checked).toEqual(false)
}) })
it(`should support Set as a checkbox model`, async () => {
const component = defineComponent({
data() {
return { value: new Set() }
},
render() {
return [
withVModel(
h('input', {
type: 'checkbox',
class: 'foo',
value: 'foo',
'onUpdate:modelValue': setValue.bind(this)
}),
this.value
),
withVModel(
h('input', {
type: 'checkbox',
class: 'bar',
value: 'bar',
'onUpdate:modelValue': setValue.bind(this)
}),
this.value
)
]
}
})
render(h(component), root)
const foo = root.querySelector('.foo')
const bar = root.querySelector('.bar')
const data = root._vnode.component.data
foo.checked = true
triggerEvent('change', foo)
await nextTick()
expect(data.value).toMatchObject(new Set(['foo']))
bar.checked = true
triggerEvent('change', bar)
await nextTick()
expect(data.value).toMatchObject(new Set(['foo', 'bar']))
bar.checked = false
triggerEvent('change', bar)
await nextTick()
expect(data.value).toMatchObject(new Set(['foo']))
foo.checked = false
triggerEvent('change', foo)
await nextTick()
expect(data.value).toMatchObject(new Set())
data.value = new Set(['foo'])
await nextTick()
expect(bar.checked).toEqual(false)
expect(foo.checked).toEqual(true)
data.value = new Set(['bar'])
await nextTick()
expect(foo.checked).toEqual(false)
expect(bar.checked).toEqual(true)
data.value = new Set()
await nextTick()
expect(foo.checked).toEqual(false)
expect(bar.checked).toEqual(false)
})
it('should work with radio', async () => { it('should work with radio', async () => {
const component = defineComponent({ const component = defineComponent({
data() { data() {

View File

@ -11,7 +11,9 @@ import {
looseEqual, looseEqual,
looseIndexOf, looseIndexOf,
invokeArrayFns, invokeArrayFns,
toNumber toNumber,
isSet,
looseHas
} from '@vue/shared' } from '@vue/shared'
type AssignerFn = (value: any) => void type AssignerFn = (value: any) => void
@ -111,6 +113,14 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
filtered.splice(index, 1) filtered.splice(index, 1)
assign(filtered) assign(filtered)
} }
} else if (isSet(modelValue)) {
const found = modelValue.has(elementValue)
if (checked && !found) {
assign(modelValue.add(elementValue))
} else if (!checked && found) {
modelValue.delete(elementValue)
assign(modelValue)
}
} else { } else {
assign(getCheckboxValue(el, checked)) assign(getCheckboxValue(el, checked))
} }
@ -132,6 +142,8 @@ function setChecked(
;(el as any)._modelValue = value ;(el as any)._modelValue = value
if (isArray(value)) { if (isArray(value)) {
el.checked = looseIndexOf(value, vnode.props!.value) > -1 el.checked = looseIndexOf(value, vnode.props!.value) > -1
} else if (isSet(value)) {
el.checked = looseHas(value, vnode.props!.value)
} else if (value !== oldValue) { } else if (value !== oldValue) {
el.checked = looseEqual(value, getCheckboxValue(el, true)) el.checked = looseEqual(value, getCheckboxValue(el, true))
} }
@ -178,10 +190,10 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
function setSelected(el: HTMLSelectElement, value: any) { function setSelected(el: HTMLSelectElement, value: any) {
const isMultiple = el.multiple const isMultiple = el.multiple
if (isMultiple && !isArray(value)) { if (isMultiple && !isArray(value) && !isSet(value)) {
__DEV__ && __DEV__ &&
warn( warn(
`<select multiple v-model> expects an Array value for its binding, ` + `<select multiple v-model> expects an Array or Set value for its binding, ` +
`but got ${Object.prototype.toString.call(value).slice(8, -1)}.` `but got ${Object.prototype.toString.call(value).slice(8, -1)}.`
) )
return return
@ -190,7 +202,11 @@ function setSelected(el: HTMLSelectElement, value: any) {
const option = el.options[i] const option = el.options[i]
const optionValue = getValue(option) const optionValue = getValue(option)
if (isMultiple) { if (isMultiple) {
if (isArray(value)) {
option.selected = looseIndexOf(value, optionValue) > -1 option.selected = looseIndexOf(value, optionValue) > -1
} else {
option.selected = looseHas(value, optionValue)
}
} else { } else {
if (looseEqual(getValue(option), value)) { if (looseEqual(getValue(option), value)) {
el.selectedIndex = i el.selectedIndex = i
@ -280,6 +296,10 @@ if (__NODE_JS__) {
if (vnode.props && looseIndexOf(value, vnode.props.value) > -1) { if (vnode.props && looseIndexOf(value, vnode.props.value) > -1) {
return { checked: true } return { checked: true }
} }
} else if (isSet(value)) {
if (vnode.props && looseHas(value, vnode.props.value)) {
return { checked: true }
}
} else if (value) { } else if (value) {
return { checked: true } return { checked: true }
} }

View File

@ -58,6 +58,9 @@ export const hasOwn = (
): key is keyof typeof val => hasOwnProperty.call(val, key) ): key is keyof typeof val => hasOwnProperty.call(val, key)
export const isArray = Array.isArray export const isArray = Array.isArray
export const isSet = (val: any): boolean => {
return toRawType(val) === 'Set'
}
export const isDate = (val: unknown): val is Date => val instanceof Date export const isDate = (val: unknown): val is Date => val instanceof Date
export const isFunction = (val: unknown): val is Function => export const isFunction = (val: unknown): val is Function =>
typeof val === 'function' typeof val === 'function'

View File

@ -51,3 +51,10 @@ export function looseEqual(a: any, b: any): boolean {
export function looseIndexOf(arr: any[], val: any): number { export function looseIndexOf(arr: any[], val: any): number {
return arr.findIndex(item => looseEqual(item, val)) return arr.findIndex(item => looseEqual(item, val))
} }
export function looseHas(set: Set<any>, val: any): boolean {
for (let item of set) {
if (looseEqual(item, val)) return true
}
return false
}