diff --git a/package.json b/package.json index 590e817..8b64c87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-recoil-form", - "version": "0.8.2", + "version": "0.9.0", "license": "MIT", "author": "Wit By Bit", "main": "dist/index.js", diff --git a/src/FormProvider.tsx b/src/FormProvider.tsx index 681187e..ad2f1b5 100644 --- a/src/FormProvider.tsx +++ b/src/FormProvider.tsx @@ -16,12 +16,14 @@ import { useSetRecoilState, useRecoilCallback, RecoilValue, + Snapshot, } from 'recoil'; import { combinedFieldAtomValues, fieldArrayColAtomValueSelectorFamily, fieldAtomFamily, formInitialValuesAtom, + formPropsOverrideAtom as formPropsOverrideAtom, formValuesAtom, getFieldArrayDataAndExtraInfo, multipleFieldsSelectorFamily, @@ -39,6 +41,8 @@ import { IFieldProps, IFieldWatchParams, IFormContextFieldInput, + IFormProps, + IFormPropsOverrideAtomValue, IIsDirtyProps, InitialValues, IRemoveFieldParams, @@ -304,6 +308,14 @@ export function useFormValues(params?: { formId?: string }) { return formValues; } +export function useSetFormProps(params?: { formId?: string }) { + const { formId: overrideFormId } = params ?? {}; + const defaultFormId = useContext(FormIdContext); + const formId = overrideFormId ?? defaultFormId; + const setFormProps = useSetRecoilState(formPropsOverrideAtom(formId)); + return setFormProps; +} + export function useFormValuesAndExtraInfos(params?: { formId?: string }) { const { formId: overrideFormId } = params ?? {}; const defaultFormId = useContext(FormIdContext); @@ -575,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, @@ -583,6 +616,8 @@ export function useFormContext(params?: { formId?: string }) { checkIsDirty, removeFields, resetInitialValues, + validateAllFields, + getValuesAndExtraInfo, }; } @@ -931,35 +966,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 = @@ -1029,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, @@ -1044,6 +1104,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(formPropsOverrideAtom(formId)) + .contents as IFormPropsOverrideAtomValue; + return validationFn?.validate; + }); function resetDataAtoms(reset: (val: RecoilState) => void) { if (formId) { @@ -1255,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] ); @@ -1313,7 +1334,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..f588e12 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -10,6 +10,7 @@ import { IFieldAtomSelectorInput, IFieldAtomValue, IFieldError, + IFormPropsOverrideAtomValue, IGetFieldArrayInput, InitialValues, } from './types'; @@ -20,6 +21,14 @@ export const formValuesAtom = atomFamily({ default: { values: {}, extraInfos: {} }, }); +export const formPropsOverrideAtom = atomFamily< +IFormPropsOverrideAtomValue, + string +>({ + key: gan('FormPropsOverride'), + 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..d710b87 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 IFormPropsOverrideAtomValue { + validate: IFormProps['validate'] | null; +}