From 3f2c02e1a605ed140db84e2dd75d597e44c04f41 Mon Sep 17 00:00:00 2001 From: Nikhil Agarwal Date: Fri, 19 Jul 2024 17:41:34 +0530 Subject: [PATCH 1/4] Add support for dynamic form validate fn --- src/FormProvider.tsx | 50 ++++++++++++++++++-------------------------- src/atoms.ts | 11 +++++++++- src/types.ts | 33 +++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/FormProvider.tsx b/src/FormProvider.tsx index 681187e..a694b9a 100644 --- a/src/FormProvider.tsx +++ b/src/FormProvider.tsx @@ -22,6 +22,7 @@ import { fieldArrayColAtomValueSelectorFamily, fieldAtomFamily, formInitialValuesAtom, + formValidationAtom, formValuesAtom, getFieldArrayDataAndExtraInfo, multipleFieldsSelectorFamily, @@ -39,6 +40,8 @@ import { IFieldProps, IFieldWatchParams, IFormContextFieldInput, + IFormProps, + IFormValidationAtomValue, IIsDirtyProps, InitialValues, IRemoveFieldParams, @@ -304,6 +307,14 @@ export function useFormValues(params?: { formId?: string }) { return formValues; } +export function useSetFormValidate(params?: { formId?: string }) { + const { formId: overrideFormId } = params ?? {}; + const defaultFormId = useContext(FormIdContext); + const formId = overrideFormId ?? defaultFormId; + const setFormValidate = useSetRecoilState(formValidationAtom(formId)); + return { setFormValidate }; +} + export function useFormValuesAndExtraInfos(params?: { formId?: string }) { const { formId: overrideFormId } = params ?? {}; const defaultFormId = useContext(FormIdContext); @@ -931,35 +942,6 @@ export function useFieldArray(props: IFieldArrayProps) { }; } -interface IFormProps { - onSubmit: (values: any, extraInfos?: any) => any; - onError?: ( - errors?: IFieldError[] | null, - formErrors?: any[] | null, - values?: any - ) => any; - initialValues?: any; - /** - * Useful in cases where you want to show the errors at the form level rather than field level - * To show field level errors, please use validate() function in useField instead - */ - validate?: (data: any) => string[] | null | undefined; - /** - * Should data be preserved if a field unmounts? - * By default, this is false - */ - skipUnregister?: boolean; - /** - * Reinitialize the form after submit back to the specified initial or empty values. - * E.g. After changing password, you want to clear all the input fields - */ - reinitializeOnSubmit?: boolean; - /** - * If true, initial values not mapped to form fields, will not come in the output - */ - skipUnusedInitialValues?: boolean; -} - const getFormValues = (formId: string, get: (val: RecoilValue) => any) => { const initialValues = get(formInitialValuesAtom(formId)) as InitialValues; const values: any = @@ -1044,6 +1026,11 @@ export function useForm(props: IFormProps) { const formId = useContext(FormIdContext); const initValuesVer = useRef(0); const isFormMounted = useRef(false); + const getValidateFnFromAtom = useRecoilCallback(({ snapshot }) => () => { + const validationFn = snapshot.getLoadable(formValidationAtom(formId)) + .contents as IFormValidationAtomValue; + return validationFn?.validate; + }); function resetDataAtoms(reset: (val: RecoilState) => void) { if (formId) { @@ -1313,7 +1300,10 @@ export function useForm(props: IFormProps) { } const { values, extraInfos } = getValuesAndExtraInfo(); const errors = validateAllFieldsInternal(values, extraInfos); - const formErrors = validate?.(values); + const laterSetFormValidationFn = getValidateFnFromAtom(); + const formErrors = laterSetFormValidationFn + ? laterSetFormValidationFn(values) + : validate?.(values); if (errors.length || formErrors?.length) { if (onError) { onError(errors, formErrors, values); diff --git a/src/atoms.ts b/src/atoms.ts index a44a495..9ef5a22 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -10,6 +10,7 @@ import { IFieldAtomSelectorInput, IFieldAtomValue, IFieldError, + IFormValidationAtomValue, IGetFieldArrayInput, InitialValues, } from './types'; @@ -20,6 +21,14 @@ export const formValuesAtom = atomFamily({ default: { values: {}, extraInfos: {} }, }); +export const formValidationAtom = atomFamily< +IFormValidationAtomValue, + string +>({ + key: gan('FormValidation'), + default: { validate: null }, +}); + export const formInitialValuesAtom = atomFamily({ key: gan('FormInitialValues'), default: { @@ -71,7 +80,7 @@ export const fieldAtomFamily = atomFamily< }, // TODO: Rename to effects for recoil 0.6 // effects_UNSTABLE is still supported and will allow older versions of recoil to work - effects_UNSTABLE: (param) => [ + effects: (param) => [ ({ onSet, node }) => { onSet((newValue) => { if (!combinedFieldAtomValues[param.formId]) { diff --git a/src/types.ts b/src/types.ts index b45fdb2..696b195 100644 --- a/src/types.ts +++ b/src/types.ts @@ -158,3 +158,36 @@ export interface IFieldError { export interface IIsDirtyProps { preCompareUpdateFormValues?: (formValues: any) => any; } + +export interface IFormProps { + onSubmit: (values: any, extraInfos?: any) => any; + onError?: ( + errors?: IFieldError[] | null, + formErrors?: any[] | null, + values?: any + ) => any; + initialValues?: any; + /** + * Useful in cases where you want to show the errors at the form level rather than field level + * To show field level errors, please use validate() function in useField instead + */ + validate?: (data: any) => string[] | null | undefined; + /** + * Should data be preserved if a field unmounts? + * By default, this is false + */ + skipUnregister?: boolean; + /** + * Reinitialize the form after submit back to the specified initial or empty values. + * E.g. After changing password, you want to clear all the input fields + */ + reinitializeOnSubmit?: boolean; + /** + * If true, initial values not mapped to form fields, will not come in the output + */ + skipUnusedInitialValues?: boolean; +} + +export interface IFormValidationAtomValue { + validate: IFormProps['validate'] | null; +} From 7dd34c38c7a16f2ce8d299527db6f63fe8b7658c Mon Sep 17 00:00:00 2001 From: Nikhil Agarwal Date: Fri, 19 Jul 2024 17:45:52 +0530 Subject: [PATCH 2/4] Publish beta version for testing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 590e817..406d8e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-recoil-form", - "version": "0.8.2", + "version": "0.9.0-beta.0", "license": "MIT", "author": "Wit By Bit", "main": "dist/index.js", From d669475420ed630ebf15fa75c6a41c2e2dae182d Mon Sep 17 00:00:00 2001 From: Nikhil Agarwal Date: Fri, 19 Jul 2024 18:55:31 +0530 Subject: [PATCH 3/4] Add validateAllFields and getValuesAndExtraInfo in useFormContext --- package.json | 2 +- src/FormProvider.tsx | 138 +++++++++++++++++++++++++++---------------- src/atoms.ts | 8 +-- src/types.ts | 2 +- 4 files changed, 92 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index 406d8e2..7d31601 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-recoil-form", - "version": "0.9.0-beta.0", + "version": "0.9.0-beta.2", "license": "MIT", "author": "Wit By Bit", "main": "dist/index.js", diff --git a/src/FormProvider.tsx b/src/FormProvider.tsx index a694b9a..ad2f1b5 100644 --- a/src/FormProvider.tsx +++ b/src/FormProvider.tsx @@ -16,13 +16,14 @@ import { useSetRecoilState, useRecoilCallback, RecoilValue, + Snapshot, } from 'recoil'; import { combinedFieldAtomValues, fieldArrayColAtomValueSelectorFamily, fieldAtomFamily, formInitialValuesAtom, - formValidationAtom, + formPropsOverrideAtom as formPropsOverrideAtom, formValuesAtom, getFieldArrayDataAndExtraInfo, multipleFieldsSelectorFamily, @@ -41,7 +42,7 @@ import { IFieldWatchParams, IFormContextFieldInput, IFormProps, - IFormValidationAtomValue, + IFormPropsOverrideAtomValue, IIsDirtyProps, InitialValues, IRemoveFieldParams, @@ -307,12 +308,12 @@ export function useFormValues(params?: { formId?: string }) { return formValues; } -export function useSetFormValidate(params?: { formId?: string }) { +export function useSetFormProps(params?: { formId?: string }) { const { formId: overrideFormId } = params ?? {}; const defaultFormId = useContext(FormIdContext); const formId = overrideFormId ?? defaultFormId; - const setFormValidate = useSetRecoilState(formValidationAtom(formId)); - return { setFormValidate }; + const setFormProps = useSetRecoilState(formPropsOverrideAtom(formId)); + return setFormProps; } export function useFormValuesAndExtraInfos(params?: { formId?: string }) { @@ -586,6 +587,27 @@ export function useFormContext(params?: { formId?: string }) { [formId] ); + const validateAllFieldsInternal = useRecoilCallback( + ({ snapshot, set }) => getValidateAllFieldsFn({ snapshot, set, formId }), + [formId] + ); + + const getValuesAndExtraInfo = useRecoilCallback( + ({ snapshot }) => + () => { + const get = (atom: RecoilValue) => + snapshot.getLoadable(atom).contents; + return getFormValues(formId, get); + }, + [] + ); + + const validateAllFields = useCallback(() => { + const { values, extraInfos } = getValuesAndExtraInfo(); + const errors = validateAllFieldsInternal(values, extraInfos); + return errors; + }, [getValuesAndExtraInfo, validateAllFieldsInternal]); + return { getValue, setValue, @@ -594,6 +616,8 @@ export function useFormContext(params?: { formId?: string }) { checkIsDirty, removeFields, resetInitialValues, + validateAllFields, + getValuesAndExtraInfo, }; } @@ -1011,6 +1035,60 @@ const getFormValues = (formId: string, get: (val: RecoilValue) => any) => { return { values, extraInfos }; }; +function getValidateAllFieldsFn(props: { + snapshot: Snapshot; + formId: string; + set: ( + recoilVal: RecoilState, + valOrUpdater: ((currVal: T) => T) | T + ) => void; +}) { + const { snapshot, formId, set } = props; + return (values: any, extraInfos: any) => { + const get = (atom: RecoilValue) => snapshot.getLoadable(atom).contents; + const errors: IFieldError[] = []; + for (const fieldAtomInfo of Object.values( + combinedFieldAtomValues[formId]?.fields ?? {} + )) { + const fieldAtom = fieldAtomFamily(fieldAtomInfo.param); + const formFieldData = get(fieldAtom) as IFieldAtomValue; + const errorMsg = formFieldData.validate?.(formFieldData.data, { + values, + extraInfos, + }); + if (errorMsg) { + set(fieldAtom, (val) => + Object.assign({}, val, { error: errorMsg, touched: true }) + ); + errors.push({ + error: errorMsg, + ancestors: fieldAtomInfo.param.ancestors, + name: fieldAtomInfo.param.name, + type: 'field', + }); + } + } + for (const fieldArrayAtomInfo of Object.values( + combinedFieldAtomValues[formId]?.fieldArrays ?? {} + )) { + const { errors: fieldArrayErrors } = getFieldArrayDataAndExtraInfo( + formId, + fieldArrayAtomInfo.param, + get, + { + isValidation: true, + set, + skipFieldCheck: true, + } + ); + if (fieldArrayErrors?.length) { + errors.push(...fieldArrayErrors); + } + } + return errors; + }; +} + export function useForm(props: IFormProps) { const { initialValues, @@ -1027,8 +1105,8 @@ export function useForm(props: IFormProps) { const initValuesVer = useRef(0); const isFormMounted = useRef(false); const getValidateFnFromAtom = useRecoilCallback(({ snapshot }) => () => { - const validationFn = snapshot.getLoadable(formValidationAtom(formId)) - .contents as IFormValidationAtomValue; + const validationFn = snapshot.getLoadable(formPropsOverrideAtom(formId)) + .contents as IFormPropsOverrideAtomValue; return validationFn?.validate; }); @@ -1242,51 +1320,7 @@ export function useForm(props: IFormProps) { ); const validateAllFieldsInternal = useRecoilCallback( - ({ snapshot, set }) => - (values: any, extraInfos: any) => { - const get = (atom: RecoilValue) => - snapshot.getLoadable(atom).contents; - const errors: IFieldError[] = []; - for (const fieldAtomInfo of Object.values( - combinedFieldAtomValues[formId]?.fields ?? {} - )) { - const fieldAtom = fieldAtomFamily(fieldAtomInfo.param); - const formFieldData = get(fieldAtom) as IFieldAtomValue; - const errorMsg = formFieldData.validate?.(formFieldData.data, { - values, - extraInfos, - }); - if (errorMsg) { - set(fieldAtom, (val) => - Object.assign({}, val, { error: errorMsg, touched: true }) - ); - errors.push({ - error: errorMsg, - ancestors: fieldAtomInfo.param.ancestors, - name: fieldAtomInfo.param.name, - type: 'field', - }); - } - } - for (const fieldArrayAtomInfo of Object.values( - combinedFieldAtomValues[formId]?.fieldArrays ?? {} - )) { - const { errors: fieldArrayErrors } = getFieldArrayDataAndExtraInfo( - formId, - fieldArrayAtomInfo.param, - get, - { - isValidation: true, - set, - skipFieldCheck: true, - } - ); - if (fieldArrayErrors?.length) { - errors.push(...fieldArrayErrors); - } - } - return errors; - }, + ({ snapshot, set }) => getValidateAllFieldsFn({ snapshot, set, formId }), [formId] ); diff --git a/src/atoms.ts b/src/atoms.ts index 9ef5a22..f588e12 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -10,7 +10,7 @@ import { IFieldAtomSelectorInput, IFieldAtomValue, IFieldError, - IFormValidationAtomValue, + IFormPropsOverrideAtomValue, IGetFieldArrayInput, InitialValues, } from './types'; @@ -21,11 +21,11 @@ export const formValuesAtom = atomFamily({ default: { values: {}, extraInfos: {} }, }); -export const formValidationAtom = atomFamily< -IFormValidationAtomValue, +export const formPropsOverrideAtom = atomFamily< +IFormPropsOverrideAtomValue, string >({ - key: gan('FormValidation'), + key: gan('FormPropsOverride'), default: { validate: null }, }); diff --git a/src/types.ts b/src/types.ts index 696b195..d710b87 100644 --- a/src/types.ts +++ b/src/types.ts @@ -188,6 +188,6 @@ export interface IFormProps { skipUnusedInitialValues?: boolean; } -export interface IFormValidationAtomValue { +export interface IFormPropsOverrideAtomValue { validate: IFormProps['validate'] | null; } From 033d600d25ad088f2111b867b69f3fd610a19885 Mon Sep 17 00:00:00 2001 From: Nikhil Agarwal Date: Fri, 19 Jul 2024 18:56:12 +0530 Subject: [PATCH 4/4] Update version to 0.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d31601..8b64c87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-recoil-form", - "version": "0.9.0-beta.2", + "version": "0.9.0", "license": "MIT", "author": "Wit By Bit", "main": "dist/index.js",