From 4ed55c4650cac2f8aee35ebf120c79d31421e51f Mon Sep 17 00:00:00 2001 From: Wanjin Noh Date: Fri, 15 Nov 2024 09:02:11 +0900 Subject: [PATCH] feat: update useFormValidator to accept reactive object Signed-off-by: Wanjin Noh --- .../composables/form-validator/index.ts | 176 +++++++----------- 1 file changed, 67 insertions(+), 109 deletions(-) diff --git a/apps/web/src/common/composables/form-validator/index.ts b/apps/web/src/common/composables/form-validator/index.ts index b51ad43f61..ffaa5dcf71 100644 --- a/apps/web/src/common/composables/form-validator/index.ts +++ b/apps/web/src/common/composables/form-validator/index.ts @@ -1,31 +1,33 @@ -import type { ComputedRef, UnwrapRef } from 'vue'; +import type { ComputedRef, UnwrapRef, Ref } from 'vue'; import { - computed, reactive, ref, + computed, reactive, ref, isRef, isReadonly, readonly, } from 'vue'; import type { TranslateResult } from 'vue-i18n'; import { clone } from 'lodash'; type ValidatorResult = boolean|TranslateResult; -// eslint-disable-next-line no-unused-vars -interface Validator { (value?: any): ValidatorResult } +export interface Validator { (value: T): ValidatorResult } type ValidationResult = boolean; -function useValueValidator( - value: T, - validator?: Validator, +export function useFieldValidator( + value: T|Ref, + validator?: Validator, immediate = false, + setter?: (val: T) => void, // setter is used to set value for readonly ref ) { - const valueRef = ref(value); + const valueRef = isRef(value) ? value : ref(value); const validationStarted = ref(immediate); - const setValue = (_value: UnwrapRef) => { + const setValue = (_value: T) => { if (!validationStarted.value) validationStarted.value = true; - valueRef.value = _value; + if (isReadonly(valueRef)) { + if (setter) setter(_value); + } else valueRef.value = _value; }; const validatorResult = computed(() => { - if (validator) return validator(valueRef.value); + if (validator) return validator(valueRef.value as T); return true; }); @@ -49,7 +51,9 @@ function useValueValidator( }); const reset = () => { - valueRef.value = value as UnwrapRef; + if (isReadonly(valueRef)) { + if (setter) setter(valueRef.value as T); + } else valueRef.value = value as UnwrapRef; validationStarted.value = immediate; }; @@ -59,12 +63,16 @@ function useValueValidator( const validate = () => { if (!validationStarted.value) validationStarted.value = true; - if (validator) valueRef.value = clone(valueRef.value); + if (validator) { + if (isReadonly(valueRef)) { + if (setter) setter(clone(valueRef.value as T)); + } else valueRef.value = clone(valueRef.value); + } return true; }; return { - value: computed>(() => valueRef.value), + value: isReadonly(valueRef) ? valueRef as Readonly> : readonly(valueRef) as Readonly>, validationResult, isInvalid, invalidText, @@ -75,59 +83,55 @@ function useValueValidator( }; } +type UnwrapRefFormValue = T extends Ref ? V : T; + type Forms = { - [K in keyof T]: ComputedRef + [K in keyof T]: Readonly>> }; type Validators = { - // eslint-disable-next-line no-unused-vars - [K in keyof T]: Validator + [K in keyof T]: Validator> + | ReturnType>> }; type ValueSetters = { - // eslint-disable-next-line no-unused-vars - [K in keyof T]: (val: T[K]) => void + [K in keyof T]: (val: UnwrapRefFormValue) => void }; type InvalidTexts = { - // eslint-disable-next-line no-unused-vars [K in keyof T]: ComputedRef }; type InvalidState = { - // eslint-disable-next-line no-unused-vars [K in keyof T]: ComputedRef }; type ValidationResults = { - // eslint-disable-next-line no-unused-vars [K in keyof T]: ComputedRef }; type Resets = { - // eslint-disable-next-line no-unused-vars [K in keyof T]: () => void }; type Validates = { - // eslint-disable-next-line no-unused-vars [K in keyof T]: () => void }; type ImmediateMap = { - // eslint-disable-next-line no-unused-vars [K in keyof T]: boolean }; /** * @param _forms * A set of form input data. - * e.g. { a: '', b: 0, c: [], d: {} } + * e.g. { a: '', b: 0, c: ref([]), d: computed(() => state.value) } + * Each form value can be a primitive value, ref, or computed value. * * @param validators - * A set of validators. - * e.g. { a: (val) => !!val, b: (val) => val > 0 ? '' : 'Invalid' } - * Validators can return boolean, string or undefined. + * A set of validator function or useFieldValidator result. + * e.g. { a: (val) => !!val, b: (val) => val > 0 ? '' : 'Invalid', c: useFieldValidator([], (val) => !!val) } + * Validator functions can return boolean, string or undefined. * When it returns boolean, true means valid, and false means invalid. * When it returns string, empty string means valid, and others mean invalid. * When it returns undefined, it means validation is not executed. @@ -140,6 +144,14 @@ type ImmediateMap = { * Otherwise, it starts validation when value is updated at least once. * Default: false * + * @param _valueSetters + * @params [_valueSetters] + * A set of value setters. + * e.g. { a: (val) => { state.a = val; } } + * Default: {} + * If the form value is readonly, it is required to set value by using setter. + * If the form value is not readonly, it is not required to set value by using setter. + * * @returns {Object} {forms, invalidState, invalidTexts, isAllValid, * setForm, resetAll, resetValidations, validateAll, validate} * forms: A set of ComputedRef form value. @@ -152,76 +164,17 @@ type ImmediateMap = { * resetValidations: Function. Reset validation states. * validateAll: Function. Validate all form values. Returns result. * validate: Function. Validate form's value by given property name. Returns result. - * - * @example - * - - - setup() { - const { - forms: { name, address }, - invalidState, - invalidTexts, - setForm, isAllValid, - } = useFormValidator({ - name: 'name', - address: '', - }, { - name: (val: string) => { - if (!val.trim()) return '이름을 작성하세요'; - return true; - }, - address: (val: string) => { - if (val.trim().length < 5) return '주소는 5자 이상 작성하세요'; - return true; - }, - }); - - const handleNameInput = (value) => { - setForm('name', value); - }; - - const handleAddressInput = () => { - setForm('address', value); - }; - - const handleReset = () => { - initForm({ - name: 'name', - address: '', - }) - }; - - return { - name, - address, - invalidState, - invalidTexts, - isAllValid, - handleNameInput, - handleAddressInput, - handleReset, - }; - } */ export function useFormValidator = any>( _forms: T, validators: Partial>, _immediate: Partial>|boolean = false, + _valueSetters: Partial> = {}, ) { const formKeys: Array = Object.keys(_forms); const forms = {} as Forms; - const valueSetters = {} as ValueSetters; + const valueSetters = { ..._valueSetters } as ValueSetters; const invalidTexts = {} as InvalidTexts; const invalidState = {} as InvalidState; const validationResults = {} as ValidationResults; @@ -229,32 +182,32 @@ export function useFormValidator = any>( const resetValidationMap = {} as Resets; const validateMap = {} as Validates; - formKeys.forEach((key) => { + formKeys.forEach((key: keyof T) => { const validator = validators[key]; const immediate = typeof _immediate === 'boolean' ? _immediate : _immediate[key]; - - const { - value, setValue, validationResult, isInvalid, invalidText, reset, resetValidation, validate, - } = useValueValidator(_forms[key], validator, immediate); - - forms[key] = value; - valueSetters[key] = setValue; - validationResults[key] = validationResult; - invalidState[key] = isInvalid; - invalidTexts[key] = invalidText; - resets[key] = reset; - resetValidationMap[key] = resetValidation; - validateMap[key] = validate; + if (validator) { + const { + value, setValue, validationResult, isInvalid, invalidText, reset, resetValidation, validate, + } = typeof validator === 'function' ? useFieldValidator(_forms[key], validator, immediate) : validator; + forms[key] = value as Forms[keyof T]; + valueSetters[key] = setValue; + validationResults[key] = validationResult; + invalidState[key] = isInvalid; + invalidTexts[key] = invalidText; + resets[key] = reset; + resetValidationMap[key] = resetValidation; + validateMap[key] = validate; + } }); const isAllValid = computed(() => Object.values(validationResults).every((validationResult) => validationResult.value)); - const setForm = (key: keyof T | T, value?: T[keyof T]) => { - if (typeof key === 'object') { - const newForm = key; - Object.keys(newForm).forEach((k) => { + const setForm = (key: keyof T | Partial, value?: T[keyof T]) => { + if (typeof key === 'object') { // if key is an object, that means, it is batch update + const newForm: Partial = key; + Object.keys(newForm).forEach((k: keyof T) => { const setter = valueSetters[k]; - if (setter) setter(newForm[k]); + if (setter) setter(newForm[k] as UnwrapRefFormValue); }); } else { const setter = valueSetters[key]; @@ -287,6 +240,10 @@ export function useFormValidator = any>( }); }; + const resetValidation = (key: keyof T) => { + if (resetValidationMap[key]) resetValidationMap[key](); + }; + const resetValidations = () => { formKeys.forEach((key) => { resetValidationMap[key](); @@ -311,6 +268,7 @@ export function useFormValidator = any>( initForm, setForm, resetAll, + resetValidation, resetValidations, validateAll, validate,