From cf1b6c666f45a284494f80981522a3dc4804a683 Mon Sep 17 00:00:00 2001 From: Pick Date: Mon, 14 Sep 2020 23:16:50 +0800 Subject: [PATCH] feat(runtime-dom): allow native Set as v-model checkbox source (#1957) --- .../__tests__/directives/vModel.spec.ts | 70 +++++++++++++++++++ packages/runtime-dom/src/directives/vModel.ts | 28 ++++++-- packages/shared/src/index.ts | 3 + packages/shared/src/looseEqual.ts | 7 ++ 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/packages/runtime-dom/__tests__/directives/vModel.spec.ts b/packages/runtime-dom/__tests__/directives/vModel.spec.ts index c8b3531ffb9..33d48e5fc6c 100644 --- a/packages/runtime-dom/__tests__/directives/vModel.spec.ts +++ b/packages/runtime-dom/__tests__/directives/vModel.spec.ts @@ -433,6 +433,76 @@ describe('vModel', () => { 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 () => { const component = defineComponent({ data() { diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 23ebb07b359..ea1098dd19b 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -11,7 +11,9 @@ import { looseEqual, looseIndexOf, invokeArrayFns, - toNumber + toNumber, + isSet, + looseHas } from '@vue/shared' type AssignerFn = (value: any) => void @@ -111,6 +113,14 @@ export const vModelCheckbox: ModelDirective = { filtered.splice(index, 1) 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 { assign(getCheckboxValue(el, checked)) } @@ -132,6 +142,8 @@ function setChecked( ;(el as any)._modelValue = value if (isArray(value)) { el.checked = looseIndexOf(value, vnode.props!.value) > -1 + } else if (isSet(value)) { + el.checked = looseHas(value, vnode.props!.value) } else if (value !== oldValue) { el.checked = looseEqual(value, getCheckboxValue(el, true)) } @@ -178,10 +190,10 @@ export const vModelSelect: ModelDirective = { function setSelected(el: HTMLSelectElement, value: any) { const isMultiple = el.multiple - if (isMultiple && !isArray(value)) { + if (isMultiple && !isArray(value) && !isSet(value)) { __DEV__ && warn( - ` expects an Array or Set value for its binding, ` + `but got ${Object.prototype.toString.call(value).slice(8, -1)}.` ) return @@ -190,7 +202,11 @@ function setSelected(el: HTMLSelectElement, value: any) { const option = el.options[i] const optionValue = getValue(option) if (isMultiple) { - option.selected = looseIndexOf(value, optionValue) > -1 + if (isArray(value)) { + option.selected = looseIndexOf(value, optionValue) > -1 + } else { + option.selected = looseHas(value, optionValue) + } } else { if (looseEqual(getValue(option), value)) { el.selectedIndex = i @@ -280,6 +296,10 @@ if (__NODE_JS__) { if (vnode.props && looseIndexOf(value, vnode.props.value) > -1) { return { checked: true } } + } else if (isSet(value)) { + if (vnode.props && looseHas(value, vnode.props.value)) { + return { checked: true } + } } else if (value) { return { checked: true } } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 69739cae251..2178c5f1ee7 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -58,6 +58,9 @@ export const hasOwn = ( ): key is keyof typeof val => hasOwnProperty.call(val, key) 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 isFunction = (val: unknown): val is Function => typeof val === 'function' diff --git a/packages/shared/src/looseEqual.ts b/packages/shared/src/looseEqual.ts index 030f0338b30..076ea7b5569 100644 --- a/packages/shared/src/looseEqual.ts +++ b/packages/shared/src/looseEqual.ts @@ -51,3 +51,10 @@ export function looseEqual(a: any, b: any): boolean { export function looseIndexOf(arr: any[], val: any): number { return arr.findIndex(item => looseEqual(item, val)) } + +export function looseHas(set: Set, val: any): boolean { + for (let item of set) { + if (looseEqual(item, val)) return true + } + return false +}