Skip to content

Commit cf1b6c6

Browse files
authored
feat(runtime-dom): allow native Set as v-model checkbox source (#1957)
1 parent 542680e commit cf1b6c6

File tree

4 files changed

+104
-4
lines changed

4 files changed

+104
-4
lines changed

packages/runtime-dom/__tests__/directives/vModel.spec.ts

+70
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,76 @@ describe('vModel', () => {
433433
expect(bar.checked).toEqual(false)
434434
})
435435

436+
it(`should support Set as a checkbox model`, async () => {
437+
const component = defineComponent({
438+
data() {
439+
return { value: new Set() }
440+
},
441+
render() {
442+
return [
443+
withVModel(
444+
h('input', {
445+
type: 'checkbox',
446+
class: 'foo',
447+
value: 'foo',
448+
'onUpdate:modelValue': setValue.bind(this)
449+
}),
450+
this.value
451+
),
452+
withVModel(
453+
h('input', {
454+
type: 'checkbox',
455+
class: 'bar',
456+
value: 'bar',
457+
'onUpdate:modelValue': setValue.bind(this)
458+
}),
459+
this.value
460+
)
461+
]
462+
}
463+
})
464+
render(h(component), root)
465+
466+
const foo = root.querySelector('.foo')
467+
const bar = root.querySelector('.bar')
468+
const data = root._vnode.component.data
469+
470+
foo.checked = true
471+
triggerEvent('change', foo)
472+
await nextTick()
473+
expect(data.value).toMatchObject(new Set(['foo']))
474+
475+
bar.checked = true
476+
triggerEvent('change', bar)
477+
await nextTick()
478+
expect(data.value).toMatchObject(new Set(['foo', 'bar']))
479+
480+
bar.checked = false
481+
triggerEvent('change', bar)
482+
await nextTick()
483+
expect(data.value).toMatchObject(new Set(['foo']))
484+
485+
foo.checked = false
486+
triggerEvent('change', foo)
487+
await nextTick()
488+
expect(data.value).toMatchObject(new Set())
489+
490+
data.value = new Set(['foo'])
491+
await nextTick()
492+
expect(bar.checked).toEqual(false)
493+
expect(foo.checked).toEqual(true)
494+
495+
data.value = new Set(['bar'])
496+
await nextTick()
497+
expect(foo.checked).toEqual(false)
498+
expect(bar.checked).toEqual(true)
499+
500+
data.value = new Set()
501+
await nextTick()
502+
expect(foo.checked).toEqual(false)
503+
expect(bar.checked).toEqual(false)
504+
})
505+
436506
it('should work with radio', async () => {
437507
const component = defineComponent({
438508
data() {

packages/runtime-dom/src/directives/vModel.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {
1111
looseEqual,
1212
looseIndexOf,
1313
invokeArrayFns,
14-
toNumber
14+
toNumber,
15+
isSet,
16+
looseHas
1517
} from '@vue/shared'
1618

1719
type AssignerFn = (value: any) => void
@@ -111,6 +113,14 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
111113
filtered.splice(index, 1)
112114
assign(filtered)
113115
}
116+
} else if (isSet(modelValue)) {
117+
const found = modelValue.has(elementValue)
118+
if (checked && !found) {
119+
assign(modelValue.add(elementValue))
120+
} else if (!checked && found) {
121+
modelValue.delete(elementValue)
122+
assign(modelValue)
123+
}
114124
} else {
115125
assign(getCheckboxValue(el, checked))
116126
}
@@ -132,6 +142,8 @@ function setChecked(
132142
;(el as any)._modelValue = value
133143
if (isArray(value)) {
134144
el.checked = looseIndexOf(value, vnode.props!.value) > -1
145+
} else if (isSet(value)) {
146+
el.checked = looseHas(value, vnode.props!.value)
135147
} else if (value !== oldValue) {
136148
el.checked = looseEqual(value, getCheckboxValue(el, true))
137149
}
@@ -178,10 +190,10 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
178190

179191
function setSelected(el: HTMLSelectElement, value: any) {
180192
const isMultiple = el.multiple
181-
if (isMultiple && !isArray(value)) {
193+
if (isMultiple && !isArray(value) && !isSet(value)) {
182194
__DEV__ &&
183195
warn(
184-
`<select multiple v-model> expects an Array value for its binding, ` +
196+
`<select multiple v-model> expects an Array or Set value for its binding, ` +
185197
`but got ${Object.prototype.toString.call(value).slice(8, -1)}.`
186198
)
187199
return
@@ -190,7 +202,11 @@ function setSelected(el: HTMLSelectElement, value: any) {
190202
const option = el.options[i]
191203
const optionValue = getValue(option)
192204
if (isMultiple) {
193-
option.selected = looseIndexOf(value, optionValue) > -1
205+
if (isArray(value)) {
206+
option.selected = looseIndexOf(value, optionValue) > -1
207+
} else {
208+
option.selected = looseHas(value, optionValue)
209+
}
194210
} else {
195211
if (looseEqual(getValue(option), value)) {
196212
el.selectedIndex = i
@@ -280,6 +296,10 @@ if (__NODE_JS__) {
280296
if (vnode.props && looseIndexOf(value, vnode.props.value) > -1) {
281297
return { checked: true }
282298
}
299+
} else if (isSet(value)) {
300+
if (vnode.props && looseHas(value, vnode.props.value)) {
301+
return { checked: true }
302+
}
283303
} else if (value) {
284304
return { checked: true }
285305
}

packages/shared/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export const hasOwn = (
5858
): key is keyof typeof val => hasOwnProperty.call(val, key)
5959

6060
export const isArray = Array.isArray
61+
export const isSet = (val: any): boolean => {
62+
return toRawType(val) === 'Set'
63+
}
6164
export const isDate = (val: unknown): val is Date => val instanceof Date
6265
export const isFunction = (val: unknown): val is Function =>
6366
typeof val === 'function'

packages/shared/src/looseEqual.ts

+7
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,10 @@ export function looseEqual(a: any, b: any): boolean {
5151
export function looseIndexOf(arr: any[], val: any): number {
5252
return arr.findIndex(item => looseEqual(item, val))
5353
}
54+
55+
export function looseHas(set: Set<any>, val: any): boolean {
56+
for (let item of set) {
57+
if (looseEqual(item, val)) return true
58+
}
59+
return false
60+
}

0 commit comments

Comments
 (0)