Skip to content

Commit

Permalink
Merge pull request #36 from witbybit/set-validate-fn
Browse files Browse the repository at this point in the history
Release 0.9.0
  • Loading branch information
nikhilag authored Jul 19, 2024
2 parents e435b59 + 033d600 commit 6fb3954
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 77 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
174 changes: 99 additions & 75 deletions src/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import {
useSetRecoilState,
useRecoilCallback,
RecoilValue,
Snapshot,
} from 'recoil';
import {
combinedFieldAtomValues,
fieldArrayColAtomValueSelectorFamily,
fieldAtomFamily,
formInitialValuesAtom,
formPropsOverrideAtom as formPropsOverrideAtom,
formValuesAtom,
getFieldArrayDataAndExtraInfo,
multipleFieldsSelectorFamily,
Expand All @@ -39,6 +41,8 @@ import {
IFieldProps,
IFieldWatchParams,
IFormContextFieldInput,
IFormProps,
IFormPropsOverrideAtomValue,
IIsDirtyProps,
InitialValues,
IRemoveFieldParams,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<any>) =>
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,
Expand All @@ -583,6 +616,8 @@ export function useFormContext(params?: { formId?: string }) {
checkIsDirty,
removeFields,
resetInitialValues,
validateAllFields,
getValuesAndExtraInfo,
};
}

Expand Down Expand Up @@ -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>) => any) => {
const initialValues = get(formInitialValuesAtom(formId)) as InitialValues;
const values: any =
Expand Down Expand Up @@ -1029,6 +1035,60 @@ const getFormValues = (formId: string, get: (val: RecoilValue<any>) => any) => {
return { values, extraInfos };
};

function getValidateAllFieldsFn(props: {
snapshot: Snapshot;
formId: string;
set: <T>(
recoilVal: RecoilState<T>,
valOrUpdater: ((currVal: T) => T) | T
) => void;
}) {
const { snapshot, formId, set } = props;
return (values: any, extraInfos: any) => {
const get = (atom: RecoilValue<any>) => 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,
Expand All @@ -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<any>) => void) {
if (formId) {
Expand Down Expand Up @@ -1255,51 +1320,7 @@ export function useForm(props: IFormProps) {
);

const validateAllFieldsInternal = useRecoilCallback(
({ snapshot, set }) =>
(values: any, extraInfos: any) => {
const get = (atom: RecoilValue<any>) =>
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]
);

Expand All @@ -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);
Expand Down
11 changes: 10 additions & 1 deletion src/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
IFieldAtomSelectorInput,
IFieldAtomValue,
IFieldError,
IFormPropsOverrideAtomValue,
IGetFieldArrayInput,
InitialValues,
} from './types';
Expand All @@ -20,6 +21,14 @@ export const formValuesAtom = atomFamily<FinalValues, string>({
default: { values: {}, extraInfos: {} },
});

export const formPropsOverrideAtom = atomFamily<
IFormPropsOverrideAtomValue,
string
>({
key: gan('FormPropsOverride'),
default: { validate: null },
});

export const formInitialValuesAtom = atomFamily<InitialValues, string>({
key: gan('FormInitialValues'),
default: {
Expand Down Expand Up @@ -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]) {
Expand Down
33 changes: 33 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit 6fb3954

Please sign in to comment.