@mongez/react-form
Version:
A Powerful React form handler.
1,862 lines (1,569 loc) • 66.1 kB
Markdown
# Mongez React Form
A Powerful React form handler to handle react forms regardless your desired UI.
Mongez React Form is a headless UI framework Form Handler, meaning it provides you with handlers to handle form and form controls and the UI is on your own.
> This documentation will be in Typescript for better illustration.
## Installation
`yarn add @mongez/react-form`
Or
`npm i @mongez/react-form`
## Usage
First off, in your entry main file, we need to set the validation translations, for example:
```tsx
// src/main.tsx
import {
enValidationTranslation,
arValidationTranslation,
} from "@mongez/react-form";
import { extend } from "@mongez/localization";
// validation object must be set with the namespace `validation`
extend("en", { validation: enValidationTranslation });
extend("ar", { validation: arValidationTranslation });
```
The package here has two main anchors, `Form` component and `useFormControl` hook.
`Form` component is the wrapper for the entire form, it will handle the form submission and data collection.
`useFormControl` hook is the hook that will be used to register the form control in the form, it is responsible for handling data and validation.
## Example
Let's see a basic example, let's create `TextInput` component
```tsx
// src/components/TextInput.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function TextInput(props: FormControlProps) {
const { value, changeValue } = useFormControl(props);
return (
<input
type="text"
value={value}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
);
}
```
Here we defined our `TextInput`, that receives props, then we use `useFormControl` hook to get our form control data and register it in the form, for now we just need to get `value` and `changeValue` from the hook.
Now let's use it in our `App.tsx`
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
export default function App() {
return (
<Form
onSubmit={{ values } => {
console.log(values);
}}
>
<TextInput name="firstName" />
<TextInput name="lastName" />
<button>Submit</button>
</Form>
);
}
```
The only required prop for any `formControl` is the `name`, it does not have to be `unique`.
Now once we click on the submit button, the `onSubmit` callback will be called with the form data, which is an object that contains all form controls values.
## Form Controls
Any component that uses `useFormControl` hook will be considered as a form control, and it will be registered in the form and it will generate a `formControl` instance, which has the following properties:
```ts
export type FormControl = {
/**
* Form input name, it must be unique
*/
name: string;
/**
* Form control type
*/
type: string;
/**
* default value
*/
defaultValue?: any;
/**
* Check if form control's value is changed
*/
isDirty: boolean;
/**
* Check if form control is touched
* Touched means that the user has focused on the input
*/
isTouched: boolean;
/**
* Form input id, used as a form input flag determiner
*/
id: string;
/**
* Form input value
*/
value: any;
/**
* Input Initial value
*/
initialValue: any;
/**
* Triggered when form starts validation
*/
validate: () => ReactNode;
/**
* Set form input error
*/
setError: (error: React.ReactNode) => void;
/**
* Determine if current control is visible in the browser
*/
isVisible: () => boolean;
/**
* Determine whether the form input is valid, this is checked after calling the validate method
* if the form control is not validated yet, then it will return null
*/
isValid: boolean | null;
/**
* List of errors caused by rules
*/
errorsList: {
[rule: string]: React.ReactNode;
};
/**
* Focus on the element
*/
focus: () => void;
/**
* Trigger blur event on the element
*/
blur: () => void;
/**
* Triggered when form resets its values
*/
reset: () => void;
/**
* Form Input Error
*/
error: React.ReactNode;
/**
* Unregister form control
*/
unregister: () => void;
/**
* Props list to this component
*/
props: any;
/**
* Check if the input's value is marked as checked
*/
checked: boolean;
/**
* Set checked value
*/
setChecked: (checked: boolean) => void;
/**
* Initial checked value
*/
initialChecked: boolean;
/**
* Determine if form control is multiple
*/
multiple?: boolean;
/**
* Collect form control value
*/
collectValue: () => any;
/**
* Check if input is collectable
*/
isCollectable: () => boolean;
/**
* Determine if form control is controlled
*/
isControlled: boolean;
/**
* Change form control value and any other related values
*/
change: (value: any, changeOptions?: FormControlChangeOptions) => void;
/**
* Determine if form control is rendered
*/
rendered: boolean;
/**
* Input Ref
*/
inputRef: any;
/**
* Visible element ref
*/
visibleElementRef: any;
/**
* Listen when form control value is changed
*/
onChange: (callback: (value: FormControlChange) => void) => EventSubscription;
/**
* Listen when form control is destroyed
*/
onDestroy: (callback: () => void) => EventSubscription;
/**
* Listen to form control when value is reset
*/
onReset: (callback: () => void) => EventSubscription;
/**
* Disable/Enable form control
*/
disable: (disable: boolean) => void;
/**
* Determine if form control is disabled
*/
disabled: boolean;
/**
* Whether unchecked value should be collected
*
* Works only if type is `checkbox` or `radio`
* @default false
*/
collectUnchecked?: boolean;
/**
* Define the value if control checked state is false, If collectUnchecked is true
*/
uncheckedValue?: any;
/**
* Any other data to be used by the form control
*/
data?: any;
};
```
## Input name
The `name` prop is the only required prop for any form control, it is used to identify the form control in the form, and will be used to get the form control value from the form data.
The input name supports a `dot` notation, which means you can create a nested object using the `dot` notation.
Most of the time you won't need to get the input name as it is being stored internally in the form control hook, but you can get it using `name` property, for example:
```tsx
<Form>
<TextInput name="user.firstName" />
<TextInput name="user.lastName" />
</Form>
```
The above example will generate the following form data:
```ts
{
user: {
firstName: "John",
lastName: "Doe"
}
}
```
> You may use `user[name]` notation instead of `user.name` notation, it will be converted into `user.name` but it is not recommended to use it.
## Input type
Input type is also required when passing props to the form control hook, for example:
```tsx
// src/components/TextInput.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function TextInput({ type = "text", props }: FormControlProps) {
const { value, changeValue } = useFormControl(props);
return (
<input
value={value}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
);
}
```
The type will be passed to the form control, if not defined it will be set to `text` by default.
## Controlled and Uncontrolled input values
You can pass `value` and `onChange` props to any form control, which means you can control the form control value from outside the form control, for example:
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
export default function App() {
const [value, changeValue] = useState("");
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<TextInput
name="firstName"
value={value}
onChange={(value) => {
changeValue(value);
}}
/>
<button>Submit</button>
</Form>
);
}
```
This will allow you control the input value from outside the form control, if you notice the `onChange` prop receives a direct value instead of an event object, this is because the form control will handle the event object and pass the value to the `onChange` prop.
You can also pass `defaultValue` prop to any form control, which means you can set the initial value of the form control, for example:
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
export default function App() {
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<TextInput name="firstName" defaultValue="John" />
<button>Submit</button>
</Form>
);
}
```
> Any form control is `controlled` internally, meaning that you'll always receive a `value` property from the `useFormControl` hook regardless of the input type, and you can change the value using the `changeValue` function.
## Getting event and other options
`onChange` as mentioned, dispatches the value directly, but you can also manage any other data that you receive from the `onChange` prop, for example:
```tsx
// src/components/TextInput.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function TextInput(props: FormControlProps) {
const { value, changeValue } = useFormControl(props);
return (
<input
value={value}
onChange={(e) => {
changeValue(e.target.value, {
event: e,
otherOption: "some value",
});
}}
/>
);
}
```
The `changeValue` function accepts a second argument which is an object that will be passed to the `onChange` prop, for example:
Now you can receive the event and other options in the `onChange` prop in the second argument, for example:
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
export default function App() {
const [value, changeValue] = useState("");
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<TextInput
name="firstName"
value={value}
onChange={(value: string, options) => {
changeValue(value);
console.log(options.event); // that property we defined in the TextInput component
}}
/>
<button>Submit</button>
</Form>
);
}
```
## Checkbox inputs
Any form control labeled with `type` equal to `checkbox` will have a slight difference in the `onChange` prop, for example:
```tsx
// src/components/Checkbox.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function Checkbox(props: FormControlProps) {
const { checked, setChecked } = useFormControl({
...props,
type: "checkbox", // must be explicitly set to checkbox
});
return (
<input
type="checkbox"
checked={checked}
onChange={(e) => {
setChecked(e.target.checked);
}}
/>
);
}
```
The `setChecked` function accepts a boolean value, which means you can pass the `checked` property of the event object to the `setChecked` function.
You can now use the `Checkbox` component in the form, for example:
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import Checkbox from "./components/Checkbox";
export default function App() {
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<Checkbox defaultChecked={true} name="rememberMe" />
<button>Submit</button>
</Form>
);
}
```
Now if we want to control the check state, we can pass the `checked` and `onChange` props to the `Checkbox` component, for example:
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import Checkbox from "./components/Checkbox";
export default function App() {
const [checked, setChecked] = useState(false);
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<Checkbox
checked={checked}
onChange={(checked) => {
setChecked(checked);
}}
name="rememberMe"
/>
<button>Submit</button>
</Form>
);
}
```
Here, the `checked` state is sent as the first argument, if you want to get the value, extract it from the second argument, for example:
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import Checkbox from "./components/Checkbox";
export default function App() {
const [checked, setChecked] = useState(false);
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<Checkbox
checked={checked}
onChange={(checked, { value }) => {
setChecked(checked);
console.log(value); // 1
}}
name="rememberMe"
/>
<button>Submit</button>
</Form>
);
}
```
You can of course assign the value if the component is checked, for example:
```tsx
// src/components/Checkbox.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function Checkbox(props: FormControlProps) {
const { checked, setChecked } = useFormControl(props);
return (
<input
type="checkbox"
checked={value}
onChange={(e) => {
setChecked(e.target.checked);
}}
/>
);
}
```
You can also set the `unchecked` value as well by passing it to `useFormControl` in the second argument object.
```tsx
// src/components/Checkbox.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function Checkbox(props: FormControlProps) {
const { checked, setChecked } = useFormControl(props, {
uncheckedValue: 0,
});
return (
<input
type="checkbox"
checked={value}
onChange={(e) => {
setChecked(e.target.checked);
}}
/>
);
}
```
## Form Control Id
Each form control must have a `unique` id, if there is no id passed in the props list, the form control hook will generate a unique id and return it, for example:
```tsx
// src/components/TextInput.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function TextInput(props: FormControlProps) {
const { value, changeValue, id } = useFormControl(props);
return (
<input
type="text"
value={value}
id={id}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
);
}
```
> In V3, the id will be by default `${name}-input` to give better accessibility, but you can still pass the id to the form control.
## useRadioInput
> Added in V3.0.0
As radio input is some sort of selection but with variant values for each radio input, `useRadioInput` will make it easier to control a single form control with multiple values from variant radio inputs.
First step is to create a `RadioGroup` component:
```tsx
import { requiredRule, RadioGroupContext, type FormControlProps } from "@mongez/react-form";
type RadioGroupProps = FormControlProps & {
children: React.ReactNode;
};
export default function RadioGroup(props\: RadioGroupProps) {
const {value, changeValue, error} = useFormControl({
...props,
rules: [requiredRule],
});
return (
<RadioGroupContext.Provider value={{
value,
changeValue
}}>
{children}
</RadioGroupContext.Provider>
);
}
```
So what we did here is we used the `RadioGroupContext` to wrap our radio inputs, then we passed the `value` and `changeValue` to the context provider.
Now let's define our `RadioInput` Component:
```tsx
import { useRadioInput } from "@mongez/react-form";
export default function RadioInput({
value,
children,
}: {
value: any;
children: React.ReactNode;
}) {
const { isSelected, changeValue } = useRadioInput(value);
return (
<label>
<input type="radio" checked={isSelected} onChange={changeValue} />
{children}
</label>
);
}
```
Now we can use the `RadioGroup` and `RadioInput` components in our form:
```tsx
import { Form } from "@mongez/react-form";
import RadioGroup from "./components/RadioGroup";
import RadioInput from "./components/RadioInput";
export default function App() {
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<RadioGroup name="gender">
<RadioInput value="male">Male</RadioInput>
<RadioInput value="female">Female</RadioInput>
</RadioGroup>
</Form>
);
```
We can mark it as a required field by passing the `required` prop to the `RadioGroup` component.
## Input Ref
Passing `inputRef` to the input that we're working on is important for handling the input focus, blur and so on
```tsx
// src/components/TextInput.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
import { useEffect } from "react";
export default function TextInput(props: FormControlProps) {
const { value, changeValue, id, inputRef, formControl } =
useFormControl(props);
useEffect(() => {
setTimeout(() => {
// focus the input after 1 second
// this requires the inputRef to be passed to the input
formControl.focus();
}, 1000);
}, []);
return (
<input
type="text"
value={value}
id={id}
ref={inputRef}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
);
}
```
You can also perform `blur` as well:
```tsx
// src/components/TextInput.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
import { useEffect } from "react";
export default function TextInput(props: FormControlProps) {
const { value, changeValue, id, inputRef, formControl } =
useFormControl(props);
useEffect(() => {
setTimeout(() => {
// focus the input after 1 second
// this requires the inputRef to be passed to the input
formControl.focus();
setTimeout(() => {
// blur the input after focusing on it with 1 second
formControl.blur();
}, 1000);
}, 1000);
}, []);
return (
<input
type="text"
value={value}
id={id}
ref={inputRef}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
);
}
```
## Disabled Prop
Form control also preserves the `disabled` prop and return it directly, for example:
```tsx
// src/components/TextInput.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function TextInput(props: FormControlProps) {
const { value, changeValue, id, disabled } = useFormControl(props);
return (
<input
type="text"
value={value}
id={id}
disabled={disabled}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
);
}
```
If you want to change the state of `disable` state, you can use `disable` and `enable` methods, for example:
```tsx
// src/components/TextInput.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function TextInput(props: FormControlProps) {
const { value, changeValue, id, disabled, disable, formControl } =
useFormControl(props);
useEffect(() => {
setTimeout(() => {
// disable the input after 1 second
disable();
// or using the formControl
formControl.disable();
}, 1000);
}, []);
return (
<input
type="text"
value={value}
id={id}
disabled={disabled}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
);
}
```
## Is Touched
> Added in v2.1.0
Is touched in terms of form control concept means that the user has focused on the input.
You can check if the form control is touched or not using `formControl.isTouched` property, for example:
```tsx
// src/components/TextInput.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function TextInput(props: FormControlProps) {
const { value, changeValue, id, disabled, disable, formControl } =
useFormControl(props);
useEffect(() => {
setTimeout(() => {
// check if the input is touched
if (formControl.isTouched) {
// do something
}
}, 1000);
}, []);
return (
<input
type="text"
value={value}
id={id}
disabled={disabled}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
);
}
```
## Is Dirty
> Added in v2.1.0
Is dirty in terms of form control concept means that the form control value is changed.
You can check if the form control is dirty or not using `formControl.isDirty` property, for example:
```tsx
// src/components/TextInput.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function TextInput(props: FormControlProps) {
const { value, changeValue, id, disabled, disable, formControl } =
useFormControl(props);
useEffect(() => {
setTimeout(() => {
// check if the input is dirty
if (formControl.isDirty) {
// do something
}
}, 1000);
}, []);
return (
<input
type="text"
value={value}
id={id}
disabled={disabled}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
);
}
```
## Getting other props
Apart from the previous props, any other prop will be sent to the input will be returned as `otherProps`, for example:
```tsx
// src/components/Checkbox.tsx
import { useFormControl, FormControlProps } from "@mongez/react-form";
export default function Checkbox(props: FormControlProps) {
const { checked, setChecked, otherProps } = useFormControl(props);
return (
<input
type="checkbox"
checked={value}
onChange={(e) => {
setChecked(e.target.checked);
}}
{...otherProps}
/>
);
}
```
## Input Validation
Now let's move to the validation part, we can split it into two parts, using `rules` or using manual validation.
### Using rules
First off, let's define the rules list that `could` be used for `TextInput` component, for example:
```tsx
// src/components/TextInput.tsx
import { Form, requiredRule } from "@mongez/react-form";
export default function TextInput({
rules = [requiredRule],
...props
}: FormControlProps) {
const { value, changeValue } = useFormControl({
...props,
rules,
});
return (
<input
type="text"
value={value}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
);
}
```
Here we defined the default `rules` that could run against the value change, now if we want to use it, we just have to pass `required` prop to the `TextInput` component, for example:
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
export default function App() {
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
>
<TextInput name="name" required />
<button>Submit</button>
</Form>
);
}
```
Now if we submitted the form, it won't go to `onSubmit` method, because the `name` input is required, and it's empty.
#### Displaying the error
If the rule is `not valid`, then it will return the error message, so we can display it in the UI, for example:
```tsx
// src/components/TextInput.tsx
import { Form, requiredRule } from "@mongez/react-form";
export default function TextInput({
rules = [requiredRule],
...props
}: FormControlProps) {
const { value, changeValue, error } = useFormControl({
...props,
rules,
});
return (
<>
<input
type="text"
value={value}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
{error && (
<span
style={{
color: "red",
}}
>
{error}
</span>
)}
</>
);
}
```
The error will appear based on current locale code from [Mongez Localization](https://github.com/hassanzohdy/mongez-localization)
For now translation supports Six languages, `English`, `Arabic`, `French`, `Spanish`, `Italian` and `Germany` with locale codes `en`, `ar`, `fr`, `es`, `it` and `de` respectively.
Let's add another rule `minLengthRule` to the `TextInput` component, for example:
```tsx
// src/components/TextInput.tsx
import { Form, requiredRule, minLengthRule } from "@mongez/react-form";
export default function TextInput(props: FormControlProps) {
const { value, changeValue, error } = useFormControl({
rules: [requiredRule, minLengthRule],
...props,
});
return (
<>
<input
type="text"
value={value}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
{error && (
<span
style={{
color: "red",
}}
>
{error}
</span>
)}
</>
);
}
```
Now to make the `minLengthRule` work, the TextInput component must receive `minLength` prop, for example:
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
export default function App() {
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
>
<TextInput name="name" required minLength={3} />
<button>Submit</button>
</Form>
);
}
```
## Rules list
Here are the available rules that you can use:
- `requiredRule`: Check if the value is not empty.
- `null`, `undefined`, `''` and `[]` are considered empty.
- Requires `required` prop to be present.
- Translation Key: `validation.required`.
- `minLengthRule`: Check if the value's length is greater than or equal to the `minLength` prop.
- Requires `minLength` prop to be present.
- Translation Key: `validation.minLength`, receives `:length` as a placeholder.
- `minLength` prop will be preserved from being passed to `otherProps`.
- Works with strings and arrays.
- `maxLengthRule`: Check if the value's length is less than or equal to the `maxLength` prop.
- Requires `maxLength` prop to be present.
- Translation Key: `validation.maxLength`, receives `:length` as a placeholder.
- `maxLength` prop will be preserved from being passed to `otherProps`.
- Works with strings and arrays.
- `lengthRule`: Check if the value's length is equal to the `length` prop.
- Requires `length` prop to be present.
- Translation Key: `validation.length`, receives `:length` as a placeholder.
- `length` prop will be preserved from being passed to `otherProps`.
- Works with strings and arrays.
- `minRule`: Check if the value is greater than or equal to the `min` prop.
- Requires `min` prop to be present.
- Translation Key: `validation.min`, receives `:min` as a placeholder.
- `min` prop will be preserved from being passed to `otherProps`.
- Works with numbers.
- `maxRule`: Check if the value is less than or equal to the `max` prop.
- Requires `max` prop to be present.
- Translation Key: `validation.max`, receives `:max` as a placeholder.
- `max` prop will be preserved from being passed to `otherProps`.
- Works with numbers.
- `emailRule`: Check if the value is a valid email.
- Translation Key: `validation.email`.
- Requires `type` prop to be `email`.
- `numberRule`: Check if the value is a valid number.
- Translation Key: `validation.number`.
- Requires `type` prop to be `number`.
- `floatRule`: Check if the value is a valid float number.
- Translation Key: `validation.float`.
- Requires `type` prop to be `float`.
- `integerRule`: Check if the value is a valid integer number.
- Translation Key: `validation.integer`.
- Requires `type` prop to be `integer`.
- `patternRule`: Check if the value matches the `pattern` prop.
- Requires `pattern` prop to be present.
- Translation Key: `validation.pattern`, receives `:pattern` as a placeholder.
- `pattern` prop will be preserved from being passed to `otherProps`.
- `alphabetRule`: Check if the value is a valid alphabet.
- Translation Key: `validation.alphabet`.
- Requires `type` prop to be `alphabet`.
- `matchRule`: Check if the value matches the value of the input with the name of the `match` prop.
- Requires `match` prop to be present.
- Translation Key: `validation.match`, receives `:matchingInput` as a placeholder.
- `match` prop will be preserved from being passed to `otherProps`.
- `url` type is also supported, you must set the input type to `url` to make it work and add `urlRule` as well.
Example of usage for each rule:
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
export default function App() {
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
>
<TextInput name="name" required />
<TextInput name="email" type="email" required />
<TextInput name="age" type="number" required />
<TextInput name="salary" type="float" required />
<TextInput name="phone" type="integer" required />
<TextInput name="password" type="password" required />
<TextInput name="confirmPassword" type="password" required match="password" />
<TextInput name="website" type="url" required />
<TextInput name="address" type="text" required minLength={10} maxLength={100} />
<TextInput name="zipCode" type="text" required length={5} />
<TextInput name="phone" type="text" required pattern={/^01[0-2|5]{1}[0-9]{8}$/} />
<TextInput name="name" required alphabet />
<button>Submit</button>
</Form>
);
}
```
```tsx
// src/components/TextInput.tsx
import {
Form,
requiredRule,
minLengthRule,
maxLengthRule,
lengthRule,
emailRule,
numberRule,
floatRule,
integerRule,
patternRule,
alphabetRule,
matchRule,
} from "@mongez/react-form";
export default function TextInput(props: FormControlProps) {
const { value, changeValue, error } = useFormControl({
rules: [
requiredRule,
minLengthRule,
maxLengthRule,
lengthRule,
emailRule,
numberRule,
floatRule,
integerRule,
patternRule,
alphabetRule,
matchRule,
],
...props,
});
return (
<>
<input
type="text"
value={value}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
{error && (
<span
style={{
color: "red",
}}
>
{error}
</span>
)}
</>
);
}
```
> This is just a demo, please make a component for each type separately, for example `EmailInput`, `NumberInput`, `FloatInput`, `IntegerInput`, `PasswordInput`, `UrlInput`, `AlphabetInput` and so on.
### Create custom rule
You can of course create a custom rule to use it among your inputs, for example:
```tsx
// src/validation/phoneNumber.ts
import { type InputRule } from "@mongez/react-form";
import { trans } from "@mongez/localization";
export const phoneNumberRule: InputRule = {
name: "phoneNumber",
requiresType: "number",
validate: ({ value, type }) => {
const regex = /^01[0-2|5]{1}[0-9]{8}$/;
if (!regex.test(value)) {
return trans("validation.phoneNumber");
}
};
```
Here is the `InputRule` interface:
```ts
export type InputRule = {
validate: (
options: InputRuleOptions
) => InputRuleResult | Promise<InputRuleResult>;
/**
* Validation rule name
*/
name?: string;
/**
* Preserved props will be used to prevent these props to be passed to `otherProps` object
*/
preservedProps?: string[];
/**
* Whether it requires a value to be called or not
*
* @default true
*/
requiresValue?: boolean;
/**
* Determine what input type to run this input against
*/
requiresType?: string;
/**
* Called when form control is initialized
*/
onInit?: (options: InitOptions) => EventSubscription | undefined;
};
```
Now you can use it in your `TextInput` component
```tsx
// src/components/TextInput.tsx
import { Form, requiredRule } from "@mongez/react-form";
import { phoneNumberRule } from "../validation/phoneNumber";
export default function TextInput({
rules = [requiredRule, phoneNumberRule],
...props
}: FormControlProps) {
const { value, changeValue, type, error } = useFormControl({
...props,
rules,
});
return (
<>
<input
type={type}
value={value}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
{error && (
<span
style={{
color: "red",
}}
>
{error}
</span>
)}
</>
);
}
```
And that's it!
Now for usage, you can use it like this:
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
export default function App() {
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<TextInput name="name" required />
<TextInput name="phone" type="phoneNumber" required />
<button>Submit</button>
</Form>
);
}
```
### Single Component Validation
Sometimes, you may need to apply a certain validation only on a certain component call, this where you can use `validate` prop for that purpose.
```tsx
// src/App.tsx
import TextInput from "./components/TextInput";
import { useState } from "react";
export default function App() {
const validateUsername = ({ value }) => {
if (!value) return; // skip the validation if the value is empty
const usernameRegex = /^[a-zA-Z0-9]+$/;
if (!usernameRegex.test(value)) {
return "Username must be alphanumeric";
}
};
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<TextInput name="name" required />
<TextInput name="username" validate={validateUsername} />
<button>Submit</button>
</Form>
);
}
```
You can also `async` the validation.
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
import { useState } from "react";
import { checkUsername } from "./api";
export default function App() {
const [isCheckingUsername, setIsCheckingUsername] = useState(false);
const validateUsername = async ({ value }) => {
if (!value) return; // skip the validation if the value is empty
// check for username from api
setIsCheckingUsername(true);
try {
await checkUsername(value);
} catch (error) {
return error.message;
} finally {
setIsCheckingUsername(false);
}
};
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<TextInput name="name" required />
<TextInput name="username" validate={validateUsername} />
<button>Submit</button>
</Form>
);
}
```
This will stop any other validator from being called until the `validateUsername` function is resolved.
#### Customizing the error message
There are multiple ways to override the error message:
1. Overriding the translation errors.
2. Changing the error keys per component call.
3. Override rule error per component call.
##### Overriding the translation errors
You can override the translation errors from the translation list using `groupedTranslations` method from [Mongez Localization](https://github.com/hassanzohdy/mongez-localization), here is the current error messages list.
```tsx
// src/locales.ts
import { groupedTranslations } from "@mongez/localization";
export const validationTranslation = {
required: {
en: "This input is required",
ar: "هذا الحقل مطلوب",
fr: "Ce champ est requis",
es: "Este campo es obligatorio",
it: "Questo campo è obbligatorio",
de: "Dieses Feld ist erforderlich",
},
invalidEmailAddress: {
en: "Invalid Email Address",
ar: "بريد الكتروني خاطئ",
fr: "Adresse e-mail invalide",
es: "Dirección de correo electrónico no válida",
it: "Indirizzo email non valido",
de: "Ungültige E-Mail-Adresse",
},
url: {
en: "Invalid URL",
ar: "رابط غير صحيح",
fr: "URL invalide",
es: "URL no válida",
it: "URL non valido",
de: "Ungültige URL",
},
min: {
en: "Value can not be lower than :min",
ar: "القيمة يجب أن لا تقل عن :min",
fr: "La valeur ne peut pas être inférieure à :min",
es: "El valor no puede ser inferior a :min",
it: "Il valore non può essere inferiore a :min",
de: "Der Wert darf nicht kleiner sein als :min",
},
max: {
en: "Value can not be greater than :max",
ar: "القيمة يجب أن لا تزيد عن :max",
fr: "La valeur ne peut pas être supérieure à :max",
es: "El valor no puede ser superior a :max",
it: "Il valore non può essere superiore a :max",
de: "Der Wert darf nicht größer sein als :max",
},
matchElement: {
en: "This input is not matching with :matchingInput",
ar: "هذا الحقل غير متطابق مع :matchingInput",
fr: "Ce champ ne correspond pas à :matchingInput",
es: "Este campo no coincide con :matchingInput",
it: "Questo campo non corrisponde a :matchingInput",
de: "Dieses Feld stimmt nicht mit :matchingInput überein",
},
length: {
en: "This input should have :length characters",
ar: "حروف الحقل يجب ان تساوي :length",
fr: "Ce champ doit avoir :length caractères",
es: "Este campo debe tener :length caracteres",
it: "Questo campo deve avere :length caratteri",
de: "Dieses Feld sollte :length Zeichen haben",
},
minLength: {
en: "This input can not be less than :length characters",
ar: "هذا الحقل يجب ألا يقل عن :length حرف",
fr: "Ce champ ne peut pas être inférieur à :length caractères",
es: "Este campo no puede ser inferior a :length caracteres",
it: "Questo campo non può essere inferiore a :length caratteri",
de: "Dieses Feld darf nicht weniger als :length Zeichen haben",
},
maxLength: {
en: "This input can not be greater than :length characters",
ar: "هذا الحقل يجب ألا يزيد عن :length حرف",
fr: "Ce champ ne peut pas être supérieur à :length caractères",
es: "Este campo no puede ser superior a :length caracteres",
it: "Questo campo non può essere superiore a :length caratteri",
de: "Dieses Feld darf nicht mehr als :length Zeichen haben",
},
pattern: {
en: "This input is not matching with the :pattern",
ar: "هذا الحقل غير مطابق :pattern",
fr: "Ce champ ne correspond pas au :pattern",
es: "Este campo no coincide con el :pattern",
it: "Questo campo non corrisponde al :pattern",
de: "Dieses Feld stimmt nicht mit dem :pattern überein",
},
number: {
en: "This input accepts only numbers",
ar: "هذا الحقل لا يقبل غير أرقام فقط",
fr: "Ce champ ne peut contenir que des chiffres",
es: "Este campo solo acepta números",
it: "Questo campo accetta solo numeri",
de: "Dieses Feld akzeptiert nur Zahlen",
},
integer: {
en: "This input accepts only integer digits",
ar: "هذا الحقل لا يقبل غير أرقام صحيحة",
fr: "Ce champ ne peut contenir que des chiffres entiers",
es: "Este campo solo acepta dígitos enteros",
it: "Questo campo accetta solo cifre intere",
de: "Dieses Feld akzeptiert nur ganze Zahlen",
},
float: {
en: "This input accepts only integer or float digits",
ar: "هذا الحقل لا يقبل غير أرقام صحيحة او عشرية",
fr: "Ce champ ne peut contenir que des chiffres entiers ou décimaux",
es: "Este campo solo acepta dígitos enteros o decimales",
it: "Questo campo accetta solo cifre intere o decimali",
de: "Dieses Feld akzeptiert nur ganze oder Dezimalzahlen",
},
alphabet: {
en: "This input accepts only alphabets",
ar: "هذا الحقل لا يقبل غير أحرف فقط",
fr: "Ce champ ne peut contenir que des lettres",
es: "Este campo solo acepta letras",
it: "Questo campo accetta solo lettere",
de: "Dieses Feld akzeptiert nur Buchstaben",
},
};
groupedTranslations("validation", validationTranslation);
```
#### Changing the error keys per component call
This coulld be useful for some rules such as the`match` rule to override the error key with the matching input name.
```tsx
// srcc/App.tsx
import { Form } from "@mongez/form";
import { TextInput } from "@mongez/form";
export default function App() {
return (
<Form>
<TextInput name="password" type="password" required minLength={8} />
<TextInput
name="confirmPassword"
match="password"
type="password"
errorKeys={{
matchingInput: "Passowrd Input",
}}
/>
</Form>
);
}
```
If the passowrd input does not match the confirm password input, the error message will be:
`This input is not matching with Passowrd Input`
If you installed [Localization React](https://github.com/hassanzohdy/mongez-react-localization) package, yoou can get benefit from passing `jsx` element instead of just plain text.
```tsx
// srcc/App.tsx
import { Form } from "@mongez/form";
import { TextInput } from "@mongez/form";
export default function App() {
return (
<Form>
<TextInput name="password" type="password" required minLength={8} />
<TextInput
name="confirmPassword"
match="password"
type="password"
errorKeys={{
matchingInput: <span className="text-danger">Passowrd Input</span>,
}}
/>
</Form>
);
}
```
#### Changing the error message per component call
You can also change the entire error message, forr example when working withe `pattern` rule, you can pass the `pattern` prop as a `RegExp` object, and then pass the `errorMessages` prop to override the error message.
```tsx
// srcc/App.tsx
import { Form } from "@mongez/form";
import { TextInput } from "@mongez/form";
export default function App() {
return (
<Form>
<TextInput
name="username"
placeholder="Username must accept only letters and numbers"
pattern={/^[a-zA-Z0-9]+$/}}
errorMessages={{
pattern: "Username must accept only letters and numbers"
}}
/>
</Form>
);
}
```
It's recommended to use [trans](https://github.com/hassanzohdy/mongez-localization#translating-keywords) function if you're web application has multiple languages.
```tsx
// srcc/App.tsx
import { Form } from "@mongez/form";
import { trans } from "@mongez/localization";
import { TextInput } from "@mongez/form";
export default function App() {
return (
<Form>
<TextInput
name="username"
placeholder="Username must accept only letters and numbers"
pattern={/^[a-zA-Z0-9]+$/}}
errorMessages={{
pattern: trans('usernamePatternError')
}}
/>
</Form>
);
}
```
## Validate all Rules
> Added in v2.2.0
By default validation rules are executed one by one, if one of them is not valid, the validation process will stop and the error message will be displayed.
To override this, pass to the second argument of `useFormControl` hook an object with `validateAll` property set to `true`.
```tsx
// src/components/TextInput.tsx
import { Form, requiredRule, minLengthRule, type FormControlProps } from "@mongez/react-form";
export default function TextInput(props:FormControlProps) {
const { value, changeValue, error } = useFormControl({
rules: [requiredRule, minLengthRule],
...props,
}, { validateAll: true });
return (
<>
<input
type="text"
value={value}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
{error && (
<span
style={{
color: "red",
}}
>
{error.map((error, index) => (
<div key={index}>{error}</div>
)}
</span>
)}
</>
);
}
```
In this case the `error` property will be an array of error messages.
## Get errors list based on rule
> Added in v2.2.0
If you want to detect what rules made the validation fail, you can use `errorsList` property from the `formControl` object.
```tsx
// src/components/TextInput.tsx
import {
Form,
requiredRule,
minLengthRule,
type FormControlProps,
} from "@mongez/react-form";
export default function TextInput(props: FormControlProps) {
const { value, changeValue, errorsList, error, formControl } = useFormControl(
{
rules: [requiredRule, minLengthRule],
...props,
}
);
return (
<>
<input
type="text"
value={value}
onChange={(e) => {
changeValue(e.target.value);
}}
/>
{error && (
<span
style={{
color: "red",
}}
>
{error}
</span>
)}
{errorsList.minLength && (
<span
style={{
color: "red",
fontSize: "16px",
fontWeight: "bold",
}}
>
{errorsList.minLength}
</span>
)}
</>
);
}
```
## Form Submission
The `onSubmit`prop is the only required prop for `Form` component, also, it will not be called until all form controls are **valid**.
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
import { useState } from "react";
export default function App() {
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<TextInput name="name" required />
<TextInput name="username" />
<button>Submit</button>
</Form>
);
}
```
If the form is not submitted **programatically**, you can gett `event` object from the `onSubmit` callback
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
export default function App() {
const submitForm = ({ values, event }) => {
const formElement = event.target;
};
return (
<Form onSubmit={submitForm}>
<TextInput name="name" required />
<TextInput name="username" />
<button>Submit</button>
</Form>
);
}
```
> Don't use `event.preventDefault()` in the `onSubmit` callback, it will be called automatically.
### Getting form values
You can get the form values from the `onSubmit` callback using the `values` property.
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
export default function App() {
const submitForm = ({ values }) => {
console.log(values);
};
return (
<Form onSubmit={submitForm}>
<TextInput name="name" required />
<TextInput name="username" />
<button>Submit</button>
</Form>
);
}
```
However, if you want to get it as `FormData`, use `formData` property instead.
```tsx
// src/App.tsx
import { Form } from "@mongez/react-form";
import TextInput from "./components/TextInput";
import createAccount from "./services/createAccount";
export default function App() {
const submitForm = ({ formData }) => {
createAccount(formData).then((response) => {
//...
});
};
return (
<Form onSubmit={submitForm}>
<TextInput name="name" required />
<TextInput name="username" />
<button>Submit</button>
</Form>
);
}
```
This is useful if you're working with `multipart/form-data` requests and want to send some files.
### Ignoring Empty Values
By default, the form will collect all form controls with its values regardlress of their values, but if you want to ignore empty values, you can pass `ignoreEmptyValues` prop to the `Form` component.
Without using `ignoreEmptyValues` pr