From 9eac351f063c2c70a8c1f7d8c0ca81db20672f32 Mon Sep 17 00:00:00 2001 From: Yerin Park <109822768+yeriiiii@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:28:31 +0900 Subject: [PATCH] feat(vs-select, vs-checkbox-set): add max/min props and rules (#219) --- .../vs-checkbox-set/VsCheckboxSet.vue | 19 +++++-- .../__tests__/vs-checkbox-set.test.ts | 41 ++++++++++++++ .../vs-checkbox-set/vs-checkbox-set-rules.ts | 23 ++++++++ .../src/components/vs-input/VsInput.vue | 13 ++++- .../src/components/vs-input/vs-input-rules.ts | 10 ---- .../src/components/vs-select/VsSelect.vue | 39 +++++++++----- .../vs-select/__tests__/vs-select.test.ts | 53 +++++++++++++++++++ .../components/vs-select/vs-select-rules.ts | 44 +++++++++++++++ .../src/components/vs-textarea/VsTextarea.vue | 13 ++++- .../vs-textarea/vs-textarea-rules.ts | 10 ---- packages/vlossom/src/utils/props.ts | 8 +++ 11 files changed, 233 insertions(+), 40 deletions(-) create mode 100644 packages/vlossom/src/components/vs-checkbox-set/vs-checkbox-set-rules.ts create mode 100644 packages/vlossom/src/components/vs-select/vs-select-rules.ts diff --git a/packages/vlossom/src/components/vs-checkbox-set/VsCheckboxSet.vue b/packages/vlossom/src/components/vs-checkbox-set/VsCheckboxSet.vue index 390ee20cb..53ac57424 100644 --- a/packages/vlossom/src/components/vs-checkbox-set/VsCheckboxSet.vue +++ b/packages/vlossom/src/components/vs-checkbox-set/VsCheckboxSet.vue @@ -69,6 +69,7 @@ import { utils } from '@/utils'; import VsInputWrapper from '@/components/vs-input-wrapper/VsInputWrapper.vue'; import VsWrapper from '@/components/vs-wrapper/VsWrapper.vue'; import { VsCheckboxNode } from '@/nodes'; +import { useVsCheckboxSetRules } from './vs-checkbox-set-rules'; import type { VsCheckboxSetStyleSet } from './types'; import type { VsCheckboxStyleSet } from '@/components/vs-checkbox/types'; @@ -88,6 +89,16 @@ export default defineComponent({ type: Function as PropType<(from: any, to: any, option: any) => Promise | null>, default: null, }, + max: { + type: [Number, String], + default: Number.MAX_SAFE_INTEGER, + validator: (value: number | string) => utils.props.checkValidNumber(name, 'max', value), + }, + min: { + type: [Number, String], + default: 0, + validator: (value: number | string) => utils.props.checkValidNumber(name, 'min', value), + }, vertical: { type: Boolean, default: false }, // v-model modelValue: { @@ -113,6 +124,8 @@ export default defineComponent({ required, rules, state, + max, + min, } = toRefs(props); const checkboxRefs: Ref = ref([]); @@ -140,11 +153,9 @@ export default defineComponent({ ref(true), ); - function requiredCheck() { - return required.value && inputValue.value && inputValue.value.length === 0 ? 'required' : ''; - } + const { requiredCheck, maxCheck, minCheck } = useVsCheckboxSetRules(required, max, min); - const allRules = computed(() => [...rules.value, requiredCheck]); + const allRules = computed(() => [...rules.value, requiredCheck, maxCheck, minCheck]); const { computedMessages, computedState, computedDisabled, computedReadonly, shake, validate, clear, id } = useInput(context, { diff --git a/packages/vlossom/src/components/vs-checkbox-set/__tests__/vs-checkbox-set.test.ts b/packages/vlossom/src/components/vs-checkbox-set/__tests__/vs-checkbox-set.test.ts index 07c6575b9..5086b6c36 100644 --- a/packages/vlossom/src/components/vs-checkbox-set/__tests__/vs-checkbox-set.test.ts +++ b/packages/vlossom/src/components/vs-checkbox-set/__tests__/vs-checkbox-set.test.ts @@ -285,6 +285,47 @@ describe('vs-checkbox-set', () => { expect(wrapper.vm.computedMessages).toHaveLength(1); expect(wrapper.html()).toContain('required'); }); + + it('최대로 선택 가능한 아이템 수를 max props를 통해 제한하고 체크할 수 있다', async () => { + // given + const wrapper: ReturnType = mount(VsCheckboxSet, { + props: { + modelValue: [], + 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }), + options: ['A', 'B', 'C'], + max: 1, + }, + }); + + // when + await nextTick(); + await wrapper.find('input[value="A"]').trigger('click'); + await wrapper.find('input[value="B"]').trigger('click'); + + // then + expect(wrapper.vm.computedMessages).toHaveLength(1); + expect(wrapper.html()).toContain('max number of items: 1'); + }); + + it('최소로 선택 가능한 아이템 수를 max props를 통해 제한하고 체크할 수 있다', async () => { + // given + const wrapper: ReturnType = mount(VsCheckboxSet, { + props: { + modelValue: [], + 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }), + options: ['A', 'B', 'C'], + min: 2, + }, + }); + + // when + await nextTick(); + await wrapper.find('input[value="A"]').trigger('click'); + + // then + expect(wrapper.vm.computedMessages).toHaveLength(1); + expect(wrapper.html()).toContain('min number of items: 2'); + }); }); describe('validate', () => { diff --git a/packages/vlossom/src/components/vs-checkbox-set/vs-checkbox-set-rules.ts b/packages/vlossom/src/components/vs-checkbox-set/vs-checkbox-set-rules.ts new file mode 100644 index 000000000..9f78fc298 --- /dev/null +++ b/packages/vlossom/src/components/vs-checkbox-set/vs-checkbox-set-rules.ts @@ -0,0 +1,23 @@ +import { Ref } from 'vue'; + +export function useVsCheckboxSetRules(required: Ref, max: Ref, min: Ref) { + function requiredCheck(v: any[]) { + return required.value && v && v.length === 0 ? 'required' : ''; + } + + function maxCheck(v: any[]) { + const limit = Number(max.value); + return v && v.length > limit ? 'max number of items: ' + max.value : ''; + } + + function minCheck(v: any[]) { + const limit = Number(min.value); + return v && v.length < limit ? 'min number of items: ' + min.value : ''; + } + + return { + requiredCheck, + maxCheck, + minCheck, + }; +} diff --git a/packages/vlossom/src/components/vs-input/VsInput.vue b/packages/vlossom/src/components/vs-input/VsInput.vue index 8bde5b527..f62551379 100644 --- a/packages/vlossom/src/components/vs-input/VsInput.vue +++ b/packages/vlossom/src/components/vs-input/VsInput.vue @@ -84,6 +84,7 @@ import { VsIcon } from '@/icons'; import { InputType } from './types'; import type { InputValueType, VsInputStyleSet } from './types'; +import { utils } from '@/utils'; const name = VsComponent.VsInput; export default defineComponent({ @@ -96,8 +97,16 @@ export default defineComponent({ styleSet: { type: [String, Object] as PropType }, autocomplete: { type: Boolean, default: false }, dense: { type: Boolean, default: false }, - max: { type: [Number, String], default: Number.MAX_SAFE_INTEGER }, - min: { type: [Number, String], default: Number.MIN_SAFE_INTEGER }, + max: { + type: [Number, String], + default: Number.MAX_SAFE_INTEGER, + validator: (value: number | string) => utils.props.checkValidNumber(name, 'max', value), + }, + min: { + type: [Number, String], + default: Number.MIN_SAFE_INTEGER, + validator: (value: number | string) => utils.props.checkValidNumber(name, 'min', value), + }, type: { type: String as PropType, default: InputType.Text }, // v-model modelValue: { diff --git a/packages/vlossom/src/components/vs-input/vs-input-rules.ts b/packages/vlossom/src/components/vs-input/vs-input-rules.ts index 4a8dac40d..1ce1d46f2 100644 --- a/packages/vlossom/src/components/vs-input/vs-input-rules.ts +++ b/packages/vlossom/src/components/vs-input/vs-input-rules.ts @@ -17,11 +17,6 @@ export function useVsInputRules( function maxCheck(v: InputValueType) { const limit = Number(max.value); - - if (isNaN(limit) || limit > Number.MAX_SAFE_INTEGER) { - return ''; - } - if (type.value === InputType.Number && typeof v === 'number' && v > limit) { return 'max value: ' + max.value; } @@ -35,11 +30,6 @@ export function useVsInputRules( function minCheck(v: InputValueType) { const limit = Number(min.value); - - if (isNaN(limit) || limit < Number.MIN_SAFE_INTEGER) { - return ''; - } - if (type.value === InputType.Number && typeof v === 'number' && v < limit) { return 'min value: ' + min.value; } diff --git a/packages/vlossom/src/components/vs-select/VsSelect.vue b/packages/vlossom/src/components/vs-select/VsSelect.vue index 18e1d01d0..ab0c15f7d 100644 --- a/packages/vlossom/src/components/vs-select/VsSelect.vue +++ b/packages/vlossom/src/components/vs-select/VsSelect.vue @@ -216,6 +216,7 @@ import { } from '@/composables'; import { useAutocomplete, useFocusControl, useInfiniteScroll, useSelectOption, useToggleOptions } from './composables'; import { VsComponent, type ColorScheme } from '@/declaration'; +import { useVsSelectRules } from './vs-select-rules'; import { VsSelectStyleSet } from './types'; import { VsIcon } from '@/icons'; import { utils } from '@/utils'; @@ -271,6 +272,28 @@ export default defineComponent({ return isValid; }, }, + max: { + type: [Number, String], + default: Number.MAX_SAFE_INTEGER, + validator: (value: number | string, props) => { + if (!props.multiple && value) { + utils.log.propError(name, 'max', 'max can only be used with multiple prop'); + return false; + } + return utils.props.checkValidNumber(name, 'max', value); + }, + }, + min: { + type: [Number, String], + default: 0, + validator: (value: number | string, props) => { + if (!props.multiple && value) { + utils.log.propError(name, 'min', 'min can only be used with multiple prop'); + return false; + } + return utils.props.checkValidNumber(name, 'min', value); + }, + }, multiple: { type: Boolean, default: false }, selectAll: { type: Boolean, @@ -308,6 +331,8 @@ export default defineComponent({ rules, selectAll, state, + max, + min, } = toRefs(props); const { emit } = context; @@ -352,19 +377,9 @@ export default defineComponent({ options.value.map((option) => ({ id: utils.string.createID(), value: option })), ); - function requiredCheck() { - if (!required.value) { - return ''; - } - - if (multiple.value) { - return inputValue.value && inputValue.value.length > 0 ? '' : 'required'; - } else { - return inputValue.value ? '' : 'required'; - } - } + const { requiredCheck, maxCheck, minCheck } = useVsSelectRules(required, max, min, multiple); - const allRules = computed(() => [...rules.value, requiredCheck]); + const allRules = computed(() => [...rules.value, requiredCheck, maxCheck, minCheck]); function onClear() { if (multiple.value) { diff --git a/packages/vlossom/src/components/vs-select/__tests__/vs-select.test.ts b/packages/vlossom/src/components/vs-select/__tests__/vs-select.test.ts index a1a8dfa31..79aab5f90 100644 --- a/packages/vlossom/src/components/vs-select/__tests__/vs-select.test.ts +++ b/packages/vlossom/src/components/vs-select/__tests__/vs-select.test.ts @@ -911,6 +911,59 @@ describe('vs-select', () => { expect(wrapper.vm.computedMessages).toHaveLength(1); expect(wrapper.html()).toContain('required'); }); + + it('multiple이 true일 때, 최대로 선택 가능한 아이템 수를 max props를 통해 제한하고 체크할 수 있다', async () => { + // given + const wrapper: ReturnType = mount(VsSelect, { + props: { + modelValue: [], + 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }), + options: ['A', 'B', 'C'], + max: 1, + multiple: true, + }, + global: { + stubs: { + teleport: true, + }, + }, + }); + + // when + await wrapper.find('.vs-select').trigger('click'); + await wrapper.findAll('.vs-option')[1].trigger('click'); + await wrapper.findAll('.vs-option')[2].trigger('click'); + + // then + expect(wrapper.vm.computedMessages).toHaveLength(1); + expect(wrapper.html()).toContain('max number of items: 1'); + }); + + it('multiple이 true일 때, 최소로 선택 가능한 아이템 수를 min props를 통해 제한하고 체크할 수 있다', async () => { + // given + const wrapper: ReturnType = mount(VsSelect, { + props: { + modelValue: [], + 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }), + options: ['A', 'B', 'C'], + min: 2, + multiple: true, + }, + global: { + stubs: { + teleport: true, + }, + }, + }); + + // when + await wrapper.find('.vs-select').trigger('click'); + await wrapper.findAll('.vs-option')[1].trigger('click'); + + // then + expect(wrapper.vm.computedMessages).toHaveLength(1); + expect(wrapper.html()).toContain('min number of items: 2'); + }); }); describe('validate', () => { diff --git a/packages/vlossom/src/components/vs-select/vs-select-rules.ts b/packages/vlossom/src/components/vs-select/vs-select-rules.ts new file mode 100644 index 000000000..2665f9fc1 --- /dev/null +++ b/packages/vlossom/src/components/vs-select/vs-select-rules.ts @@ -0,0 +1,44 @@ +import { Ref } from 'vue'; + +export function useVsSelectRules( + required: Ref, + max: Ref, + min: Ref, + multiple: Ref, +) { + function requiredCheck(v: any) { + if (!required.value) { + return ''; + } + + if (multiple.value) { + return v && v.length > 0 ? '' : 'required'; + } else { + return v ? '' : 'required'; + } + } + + function maxCheck(v: any) { + const limit = Number(max.value); + if (multiple.value) { + return v && v.length > limit ? 'max number of items: ' + max.value : ''; + } + + return ''; + } + + function minCheck(v: any) { + const limit = Number(min.value); + if (multiple.value) { + return v && v.length < limit ? 'min number of items: ' + min.value : ''; + } + + return ''; + } + + return { + requiredCheck, + maxCheck, + minCheck, + }; +} diff --git a/packages/vlossom/src/components/vs-textarea/VsTextarea.vue b/packages/vlossom/src/components/vs-textarea/VsTextarea.vue index 7669cde77..a24dd532a 100644 --- a/packages/vlossom/src/components/vs-textarea/VsTextarea.vue +++ b/packages/vlossom/src/components/vs-textarea/VsTextarea.vue @@ -55,6 +55,7 @@ import { VsComponent, StringModifiers, type ColorScheme } from '@/declaration'; import { useVsTextareaRules } from './vs-textarea-rules'; import VsInputWrapper from '@/components/vs-input-wrapper/VsInputWrapper.vue'; import VsWrapper from '@/components/vs-wrapper/VsWrapper.vue'; +import { utils } from '@/utils'; import type { InputValueType, VsTextareaStyleSet } from './types'; @@ -68,8 +69,16 @@ export default defineComponent({ colorScheme: { type: String as PropType }, styleSet: { type: [String, Object] as PropType }, autocomplete: { type: Boolean, default: false }, - max: { type: [Number, String], default: Number.MAX_SAFE_INTEGER }, - min: { type: [Number, String], default: Number.MIN_SAFE_INTEGER }, + max: { + type: [Number, String], + default: Number.MAX_SAFE_INTEGER, + validator: (value: number | string) => utils.props.checkValidNumber(name, 'max', value), + }, + min: { + type: [Number, String], + default: 0, + validator: (value: number | string) => utils.props.checkValidNumber(name, 'min', value), + }, // v-model modelValue: { type: String, default: '' }, modelModifiers: { diff --git a/packages/vlossom/src/components/vs-textarea/vs-textarea-rules.ts b/packages/vlossom/src/components/vs-textarea/vs-textarea-rules.ts index 06aade190..db7ae21ac 100644 --- a/packages/vlossom/src/components/vs-textarea/vs-textarea-rules.ts +++ b/packages/vlossom/src/components/vs-textarea/vs-textarea-rules.ts @@ -12,11 +12,6 @@ export function useVsTextareaRules(required: Ref, max: Ref Number.MAX_SAFE_INTEGER) { - return ''; - } - if (typeof v === 'string' && v.length > limit) { return 'max length: ' + max.value; } @@ -26,11 +21,6 @@ export function useVsTextareaRules(required: Ref, max: Ref= Number.MIN_SAFE_INTEGER && num <= Number.MAX_SAFE_INTEGER; + if (!isValid) { + logUtil.propError(componentName, property, `invalid ${property} value`); + } + return isValid; + }, };