@cartoonmangodev/react-form-handler is a React library for simplifying form validation. It provides a set of utilities and hooks to handle client-side form validation with ease.
Declarative Validation Rules: Define validation rules in a declarative manner.
Dynamic Validation: Handle dynamic validation based on user input.
Custom Validation Functions: Support for custom validation functions.
This package requires React 16.8.4 or later.
Use the package manager npm or yarn to install @cartoonmangodev/react-form-handler.
$ npm install @cartoonmangodev/react-form-handler
or
$ yarn add @cartoonmangodev/react-form-handler
/* form.js */
import { FormProvider } from "@cartoonmangodev/react-form-handler";
export const { useForm, useFormRef } = FormProvider();
/* hook.js */
import { useForm } from "./form.js";
export const useFormHook = () =>
useForm({
FORM_CONFIG: {
name: { isRequired: true },
age: { min: 18, max: 16 },
},
});
/* customInputField.js */
import { useFormConsumer } from "@cartoonmangodev/react-form-handler";
const InputField = React.memo((props) => {
const { id, name, ...restProps } = props;
const { inputProps } = useFormConsumer({ id });
return (
<div>
<div>{name}</div>
<input {...inputProps} {...restProps} />
{inputProps.error && <span>{inputProps.error}</span>}
</div>
);
});
/* basicForm.js */
import { useEffect, useRef } from "react";
import { Form } from "@cartoonmangodev/react-form-handler";
import { useFormHook } from "./hook.js";
import { InputField } from "./customInputField.js";
export const BasicForm = () => {
const { formRef, formId } = useFormHook();
return (
<Form.Provider formRef={formRef}>
<InputField id="name" />
<InputField id="age" />
<Button onClick={formRef.validateForm}>Submit</Button>
</Form.Provider>
);
};
Note:
- Its required to configure form Provider before start using useForm hook
- Global config (one time configuration)
/* form.js */
import { FormProvider, Form } from "@cartoonmangodev/react-form-handler";
import {
ON_CHANGE,
ON_BLUR,
ON_CHANGE,
ERROR,
ON_CHANGE_TEXT,
} from "@cartoonmangodev/react-form-handler/constants";
const { useForm, useFormRef } = FormProvider({
ON_CHANGE_KEY: ON_CHANGE /* use ON_CHANGE_TEXT if you are using react-native */,
ON_BLUR_KEY: ON_BLUR,
VALUE_KEY: VALUE,
ERROR_KEY: ERROR,
});
export { useForm, useFormRef, Form };
useForm:
A hook for managing form state.
useFormRef:
A hook for obtaining a reference to the form.
Form:
A form component for use in React components.
/* customInputField.js */
import { useFormConsumer } from "@cartoonmangodev/react-form-handler";
const InputField = React.memo((props) => {
const { id, name, ...restProps } = props;
const { inputProps } = useFormConsumer({ id });
return (
<div>
<div>{name}</div>
<input {...inputProps} {...restProps} />
{inputProps.error && <span>{inputProps.error}</span>}
</div>
);
});
- One significant advantage of using the context-based implementation provided by @cartoonmangodev/react-form-handler
is its ability to minimize unnecessary component re-renders on every onChange.
- By managing form state through the FormProvider, components subscribing to the form state will only re-render when relevant form data changes. This can lead to improved performance in scenarios where frequent re-renders are not desired.
/* customInputField.js */
import { Form } from "@cartoonmangodev/react-form-handler";
export const InputField = React.memo((props) => {
const { id, name, ...restProps } = props;
return (
<Form.Consumer
id={id}
inputConfig={{
isRequired: false,
}}
>
{({ inputProps }) => (
<div>
<div>{name}</div>
<input {...inputProps} {...restProps} />
{inputProps.error && <span>{inputProps.error}</span>}
</div>
)}
</Form.Consumer>
);
});
/* hook.js */
import { useForm } from "./form.js";
Define your form configuration using FORM_CONFIG
. Each field in the form has specific validation rules.
const FORM_CONFIG = {
name: { isRequired: true },
age: { min: 18, max: 16 },
company: { isRequired: true },
};
Initialize the form hook using useForm
and provide the FORM_CONFIG
and initial state.
const initialState = {
name: "",
}; /* optional - default state */
export const useFormHook = () =>
useForm({
FORM_CONFIG,
initialState,
});
import { useForm } from "./form.js";
const FORM_CONFIG = {
name: { isRequired: true },
age: { min: 18, max: 16 },
company: { isRequired: true },
};
export const useFormHook = () =>
useForm({
FORM_CONFIG,
initialState: {
name: "",
},
});
/* basicForm.js */
import { useEffect, useRef } from "react";
import { useFormHook, Form } from "./hook.js";
import { InputField } from "./customInputField.js";
The BasicForm component utilizes the useFormHook
to manage form state and the Form.
Form.Provider
to wrap the form elements.
const BasicForm = () => {
const { formRef, formId } = useFormHook();
return (
<Form.Provider formRef={formRef}>
<InputField id="name" />
<InputField id="age" />
<InputField id="company" />
<Button onClick={formRef.validateForm}>Submit</Button>
</Form.Provider>
);
};
/* basicForm.js */
import { useEffect, useRef } from "react";
import { useFormHook, Form } from "./hook.js";
import { InputField } from "./customInputField.js";
const BasicForm = () => {
const { formRef, formId } = useFormHook();
return (
<Form.Provider formRef={formRef}>
<InputField id="name" />
<InputField id="age" />
<InputField id="company" />
<Button onClick={formRef.validateForm}>Submit</Button>
</Form.Provider>
);
};
export BasicForm;
/* basicForm.js */
import { newSchema } from "@cartoonmangodev/react-form-handler";
import { useForm } from "./form.js";
Define your nested form configuration using newSchema
. In this example, we have a person schema with nested properties.
const FORM_CONFIG = {
person: newSchema({
name: { isRequired: true },
age: { min: 18, max: 16 },
company: {
isRequired: true,
inputProps: {
disabled: true,
},
},
}),
};
Initialize the form hook using useForm
and provide the FORM_CONFIG
with nested schema.
export const useFormHook = () =>
useForm({
FORM_CONFIG,
});
/* hook.js */
import { newSchema } from "@cartoonmangodev/react-form-handler";
import { useForm } from "./form.js";
const FORM_CONFIG = {
person: newSchema({
name: { isRequired: true },
age: { min: 18, max: 16 },
company: { isRequired: true },
}),
};
export const useFormHook = () =>
useForm({
FORM_CONFIG,
});
/* schemaForm.js */
import { useEffect, useRef } from "react";
import { useFormHook, Form } from "./hook.js";
import { InputField } from "./customInputField.js";
const schemaForm = () => {
const { formRef, formId } = useFormHook();
console.log(formRef);
const submit = useCallback(() => {
console.log(formRef.validateForm());
}, []);
return (
<Form.Provider formRef={formRef}>
<Form.Provider id="person">
<InputField id="name" />
<InputField id="age" />
<InputField id="company" />
</Form.Provider>
<Button onClick={submit}>Submit</Button>
</Form.Provider>
);
};
- This is the image from console FormRef object
- This is the image from console after submitting form
/* hook.js */
import { newFormArray } from "@cartoonmangodev/react-form-handler";
import { useForm } from "./form.js";
const FORM_CONFIG = {
person: newFormArray({
name: { isRequired: true },
age: { min: 18, max: 16 },
comapny: { isRequired: true },
}),
};
export const useFormHook = () =>
useForm({
FORM_CONFIG,
});
/* schemaForm.js */
import { useEffect, useRef } from "react";
import { useFormHook, Form } from "./hook.js";
import { InputField } from "./customInputField.js";
const schemaForm = () => {
const { formRef, formId } = useFormHook();
console.log(formRef);
const submit = useCallback(() => {
console.log(formRef.validateForm());
}, []);
return (
<Form.Provider formRef={formRef}>
<Form.Multiple id="person">
{({ formRef: _formRef, form, formId, count, index }, arrayProps) => (
<Form.Provider formRef={_formRef} key={formId}>
<InputField id="name" />
<InputField id="age" />
<InputField id="company" />
<button onClick={form.append}>> append</button>
<button onClick={form.prepend}>prepend</button>
<button onClick={form.delete}>delete</button>
<button onClick={form.reset}>reset</button>
<button onClick={form.clear}>clear</button>
<button onClick={form.move}>move</button>
<button onClick={form.swap}>swap</button>
<button onClick={form.insert}>insert</button>
<button onClick={form.clone}>clone</button>
{console.log(arrayProps)}
</Form.Provider>
)}
</Form.Multiple>
<Button onClick={submit}>Submit</Button>
</Form.Provider>
);
};
- The
useFormHook
is used to obtain the form reference and form ID.- The
Form.Provider
wraps the entire form and provides context for form handling.- The
Form.Multiple
component is used to handle a dynamic array of form elements.- The
InputField
component is used for each form field within the dynamic array.- Various form manipulation buttons (
append
,prepend
,delete
,reset
,clear
,move
,swap
,insert
,clone
,etc.) are provided to showcase the dynamic form functionality.- The
submit
function is a callback that triggers form validation and logs the result.
Form Array methods
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
console.log(formRef.getValues());
console.log(formRef.getErrors());
};
Props | Default Value | type | description |
---|---|---|---|
onChange | function | function | A callback function triggered when the value of the component changes. |
onBlur | function | function | A callback function triggered when the component loses focus (on blur). |
value | any | any | The current value of the input |
error | string | string | An error message associated with the input |
Props | Default Value | type | value | Description |
---|---|---|---|---|
isRequired | false | Boolean | true or false |
A boolean indicating whether the field is required. Throws an error if the field is empty. |
optional | false | Boolean | true or false |
A boolean indicating whether validation should occur only if the field has a value. |
min | null | number | 4 | A number indicating the minimum value required. |
max | null | number | 8 | A number indicating the maximum value required. |
maxLength | null | number | 7 | A number indicating the maximum characters allowed. |
minLength | null | number | 6 | A number indicating the minimum characters required. |
allowOnlyNumber | false | Boolean | true or false |
A boolean indicating whether only numbers are allowed. |
allowValidNumber | false | number | true or false |
A number indicating whether the entered number is valid or not. |
type | null | number | "email","number" |
A string indicating the type of validation ("email" , "number" ). |
trim | null | Boolean | true or false |
A boolean indicating whether spaces are allowed. |
pattern | null | regex | /\d+/ |
A regular expression for custom pattern validation. |
message | null | object | {min: 'mimimum 8 characters required'} |
An object providing custom error messages for different validation scenarios |
validator | null | function | (value) => ({error: value < 5 ? 'Error' : null,value}) |
A function for custom validation logic. |
callback | null | function | ({value}) => {console.log('do something')} |
A function called after validation. |
isValidateOnBlur | true | Boolean | true or false |
A boolean indicating whether to validate the field on blur. |
isValidateOnChange | true | Boolean | true or false |
A boolean indicating whether to validate the field on change |
vatidate | true | Boolean | true or false |
A boolean indicating whether to set validation to true or false. |
default | '' | any | string or object or number |
The default value for the field |
Get form values using getValues method
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
console.log(formRef.getValues());
};
Get form errors using getErrors method
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
console.log(formRef.getErrors());
};
clearForm will reset the form values to default
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
formRef.clearForm();
};
resetForm will reset the form values to initialState
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
formRef.resetForm();
};
getFormRef will be used for nested form to access nested form ref
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
formRef.getFormRef(["person"]);
};
getFormRefById similar to useFormRef for accessing formRef based on formId
/* hook.js */
const form = ({ formId }) => {
const { formRef: nestedFormRef, formId } = formRef.getFormRefById(formId);
console.log(nestedFormRef);
};
renderForm will manually render the component
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
formRef.renderForm();
};
setFormValues will be used for prefill form values
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const IS_RESET_FORM = true;
formRef.setFormValues(
{
name: "Person",
},
IS_RESET_FORM
);
};
setInitialState will be used for set form values
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
formRef.setInitialState({
name: "Person",
});
};
validateForm will be used to validate the form
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const onSubmit = () => {
console.log(formRef.validateForm());
};
};
validateForm will be used to reset values based on keys
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
formRef.resetValues(["name"]);
};
clearValues will be used to clear values based on keys
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
formRef.clearValues(["name"]);
};
deleteFormConfig will be used to delete the formConfig
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
formRef.deleteFormConfig(["age"]);
};
modifyFormConfig will be used to modify the formConfig
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
formRef.modifyFormConfig({
age: {
type: "number",
},
});
};
resetFormConfig will be used to reset the formConfig and set the newConfig
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
formRef.modifyFormConfig({
age: {
type: "number",
},
});
};
append will add the new form after the target Form
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const { append } = formRef.formArrayProps;
const onAdd = () => {
const targetFormId = __formId__;
const values = [{ name: "steve" }];
const noOfFormsToBeAdded = 2;
append(targetFormId, values, noOfFormsToBeAdded);
};
};
prepend will add the new form before the target Form
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const { prepend } = formRef.formArrayProps;
const onAddBefore = () => {
const targetFormId = __formId__;
const values = [{ name: "steve" }];
const noOfFormsToBeAdded = 2;
prepend(targetFormId, values, noOfFormsToBeAdded);
};
};
delete method will be used to delete the target form
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const { prepend, delete: deleteForm } = formRef.formArrayProps;
const onDeleteForm = () => {
const targetFormId = [__formId1__, __formId2__];
const values = [{ name: "steve" }];
deleteForm(targetFormId);
};
};
clone method will be used to clone the target form
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const { prepend, clone } = formRef.formArrayProps;
const onCloneForm = () => {
const sourceFormId = __formId1__;
const noOfFormsToBeCloned = 2;
clone(sourceFormId, noOfFormsToBeCloned);
};
};
clear method will be used to clear the target form
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const { clear } = formRef.formArrayProps;
const onClearForm = () => {
const formIds = [__formId1__];
clear(formIds);
};
};
reset method will be used to reset the target form
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const { prepend, reset } = formRef.formArrayProps;
const onResetForm = () => {
const formIds = [__formId1__];
reset(formIds);
};
};
move method will be used to move the target form to particular index position
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const { prepend, move } = formRef.formArrayProps;
const onMoveForm = () => {
const currentFormId = __formId1__;
const targetFormId = __formId2__;
move(currentFormId, targetFormId);
};
};
move method will be used to swap between two forms
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const { prepend, swap } = formRef.formArrayProps;
const onSwapForm = () => {
const currentFormId = __formId1__;
const targetFormId = __formId2__;
forms(currentFormId, targetFormId);
};
};
insert method will be used to insert the new form on particular index
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const { prepend, insert } = formRef.formArrayProps;
const onInsertForm = () => {
const targetFormId = __formId__;
const values = [{ name: "steve" }];
const noOfFormsToBeInserted = 2;
insert(targetFormId, values, noOfFormsToBeInserted);
};
};
setFormRefArray method will be used to change the order of the form and set the new form array
/* hook.js */
import { useFormRef } from "./hook.js";
const form = ({ formId }) => {
const { formRef, formId } = useFormRef(formId);
const { setFormRefArray } = formRef;
const onSetFormRefArray = () => {
setFormRefArray([formRef, formRef]);
};
};
/* hook.js */
import { newSchema } from "@cartoonmangodev/react-form-handler";
import { useForm } from "./form.js";
const FORM_CONFIG = {
person: newSchema({
name: { isRequired: true },
age: { min: 18, max: 16 },
comapny: { isRequired: true },
}),
};
const RENDER_FORM = true;
export const useFormHook = () =>
useForm({
FORM_CONFIG,
renderForm: RENDER_FORM,
});
/* step_1.js */
import { useEffect, useRef } from "react";
import { useFormHook, Form } from "./hook.js";
import { InputField } from "./customInputField.js";
const Form = () => {
const { formRef, formId } = useFormHook();
const submit = useCallback(() => {
if (formRef.validateForm().isValid) {
// navigate("step_2", {
// formId,
// });
}
}, []);
return (
<Form.Provider formRef={formRef} dontResetOnUnmount>
<Form.Provider id="step_1">
<Form.Provider id="person">
<InputField id="name" />
<InputField id="age" />
<InputField id="company" />
</Form.Provider>
</Form.Provider>
<Button onClick={submit}>Submit</Button>
</Form.Provider>
);
};
/* step_2.js */
import { useEffect, useRef } from "react";
import { useFormHook, Form } from "./hook.js";
import { InputField } from "./customInputField.js";
const Form = ({ formId }) => {
const { formRef } = useFormRef(formId);
const submit = useCallback(() => {
console.log(formRef.validateForm());
}, []);
return (
<Form.Provider formRef={formRef} dontResetOnUnmount>
<Form.Provider id="step_2">
<Form.Provider id="person">
<InputField id="name" />
<InputField id="age" />
<InputField id="company" />
</Form.Provider>
</Form.Provider>
<Button onClick={submit}>Submit</Button>
</Form.Provider>
);
};
This will helps to controll multiple input props
/* step_1.js */
import { useEffect, useRef } from "react";
import { useFormHook, Form } from "./hook.js";
import { InputField } from "./customInputField.js";
const Form = () => {
const { formRef, formId } = useFormHook();
const submit = useCallback(() => {
if (formRef.validateForm().isValid) {
// navigate("step_2", {
// formId,
// });
}
}, []);
return (
<Form.Provider formRef={formRef} dontResetOnUnmount>
<Form.Provider id="step_1">
<Form.Provider id="person">
{({ values }) => (
<>
<InputField id="name" />
<Form.Controller disabled={!values.name}>
<InputField id="age" />
<InputField id="company" />
</Form.Controller>
</>
)}
</Form.Provider>
</Form.Provider>
<Button onClick={submit}>Submit</Button>
</Form.Provider>
);
};
Yes, this package will support for both react and react-native
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
Copyright (c) 2023-present Chrissie Fernando