Introduction
Mongez React Form (MRF) is an agnostic UI framework to manage forms and form controls.
Formik, React Hook Form
Actually, MRF differs from Formik or React Hook Form in many aspects, such as simplicity, clean code writing, provides most validation rules you might need in your form inputs and custom ones as well, it simply make your life easier, let's have a look.
Installation
yarn add @mongez/react-form
Or
npm i @mongez/react-form
This package depends on Mongez Localization and Mongez Validator for validation and message conversion, they are dependencies to the package so you don't have to setup it separately.
Usage
For form validation messages, do not forget to import your locale validation object into Mongez Localization.
// anywhere early in your app
import { enTranslation } from "@mongez/validator";
import { extend } from "@mongez/localization";
extend("en", enTranslation);
Please check Validation Messages Section which contains all available locales and current available rules list.
Now, Let's start with our main component, the Form component.
// LoginPage.tsx
import React from "react";
import { Form } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent) => {
//
};
return (
<Form onSubmit={performLogin}>
<input type="email" name="email" placeholder="Email Address" />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
So far nothing special happens here, a simple form with two inputs, except that Form do some extra functions than the normal form.
The first feature here is Form prevents default behavior that submits the form, the form will be submitted but not no redirection happens.
Now let's get the form inputs values.
// LoginPage.tsx
import React from "react";
import { Form } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent) => {
//
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<input type="email" name="email" placeholder="Email Address" />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
The only thing that is added here is collectValuesFromDOM which collects all inputs values from the dom directly if input has name attribute.
// LoginPage.tsx
import React from "react";
import { Form, FormInterface } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
console.log(form.values()); // {email: written-value, password: written-value }
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<input type="email" name="email" placeholder="Email Address" />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
In this step, the onSubmit accepts two arguments, the event handler which is the default one, and the Form class as second argument.
We called form.values(), this method collects values from the dom inputs and return an object that has all values, for the time being this works thanks to collectValuesFromDOM otherwise it will return an empty object.
Form Context
You may access the form class from any child component using FormContext
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { Form, FormInterface } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
console.log(form.values()); // {email: written-value, password: written-value }
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<EmailInput name="email" placeholder="Email Address" />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
// EmailInput.tsx
import React from "react";
import { FormContext } from "@mongez/react-form";
export default function EmailInput(props) {
const { form } = React.useContext(FormContext);
return <input type="email" {...props} />;
}
Please note that if there is no
Formcomponent in the parent tree, thenFormContextwill return null.
useForm Hook
Another way to access form class is to use useForm hook.
// EmailInput.tsx
import React from "react";
import { useForm } from "@mongez/react-form";
export default function EmailInput(props) {
const { form } = useForm();
return <input type="email" {...props} />;
}
Please note that if there is no
Formcomponent in the parent tree, thenuseFormwill return null.
Create a heavy form input
Now let's go more deeper, Let's update our EmailInput component using useFormInput Hook.
// EmailInput.tsx
import React from "react";
import { useFormInput } from "@mongez/react-form";
export default function EmailInput(props) {
const { name, id } = useFormInput(props);
console.log(id, name); // something like el-6BUxp8 email
return <input type="email" name={name} />;
}
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { Form, FormInterface } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<EmailInput name="email" />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
This will automatically register the component to our Form Component so we can collect its value from it directly.
The FormInput Object
Each component uses useFormInput hook gets a FormControl object declared in useFormInput hook.
Let's look at the available props in that object then see why this formInput exists.
import { RuleResponse } from "@mongez/validator";
import { EventSubscription } from "@mongez/events";
/**
* Available control modes
*/
type ControlMode = "input" | "button";
/**
* Form control events that can be subscribed to by the form control
*/
type FormControlEvent =
| "change"
| "reset"
| "disabled"
| "unregister"
| "validation.start"
| "validation.success"
| "validation.error"
| "validation.end";
/**
* Available control types
*/
type ControlType =
| "text"
| "color"
| "date"
| "time"
| "dateTime"
| "email"
| "checkbox"
| "radio"
| "hidden"
| "number"
| "password"
| "range"
| "search"
| "tel"
| "url"
| "week"
| "select"
| "autocomplete"
| "file"
| "image"
| "button"
| "reset"
| "submit";
type FormControl = {
/**
* Form input name, it must be unique
*/
name: string;
/**
* Form control mode
*/
control: ControlMode;
/**
* Form control type
*/
type: ControlType;
/**
* Form input id, used as a form input flag determiner
*/
id?: string;
/**
* Form input value
*/
value?: any;
/**
* Old Form control value
*/
oldValue?: any;
/**
* Triggered when form is changing disabling / enabling mode
*/
disable?: (isDisabling: boolean) => void;
/**
* Triggered when form is changing read only mode
*/
readOnly?: (isReadingOnly: boolean) => void;
/**
* Triggered when form is changing a value to the form input
*/
changeValue?: (newValue: any) => void;
/**
* Triggered when form input value is changed
*/
onChange?: (newValue: any) => void;
/**
* Triggered when form starts validation
*/
validate?: (newValue?: string) => RuleResponse | null;
/**
* Set form input error
*/
setError: (error: RuleResponse) => void;
/**
* Determine whether the form input is valid, this is checked after calling the validate method
*/
isValid?: boolean;
/**
* Determine whether form input is disabled
*/
isDisabled?: boolean;
/**
* Determine whether form input is in read only state
*/
isReadOnly?: boolean;
/**
* Determine whether form input's value has been changed
*/
isDirty?: boolean;
/**
* Focus on the element
*/
focus?: (focus: boolean) => void;
/**
* Triggered when form resets its values
*/
reset?: () => void;
/**
* Form Input Error
*/
error?: RuleResponse | null;
/**
* Form control event listener
*/
on: (event: FormControlEvent, callback: any) => EventSubscription;
/**
* Trigger Event
*/
trigger: (event: FormControlEvent, ...values: any[]) => void;
/**
* Unregister form control
*/
unregister: () => void;
/**
* Props list to this component
*/
props?: any;
};
The main responsibility for the form control is to be registered in Form Class, so form can communicate with any inner components easily.
We'll learn more through the rest of the article though.
The id attribute
In this example, we used useFormInput and return an object that has name and id props, but why did id prop is returned?
useFormInput wil generate a unique id for the component if no id prop is passed, which will be something like el-aW313EDq.
The name attribute
But why to get the name from useFormInput rather than getting it from props object directly?
useFormInput will manipulate the name if passed to the component props as it allows using dot.notation syntax.
Behind the scenes, this is handled using toInputName utility in Mongez Reinforcements.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { Form, FormInterface } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<EmailInput name="user.email" />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
The email input will be changed into user[name] which is a more standard name attribute.
Controlled Vs Uncontrolled Component
useFormInput allows you to use both types of components, however, there will other feature that comes with both types, the value validation.
Uncontrolled Component
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { Form, FormInterface } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<EmailInput name="email" defaultValue="Initial Email Value" />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
Controlled Component
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { Form, FormInterface } from "@mongez/react-form";
export default function LoginPage() {
const [email, setEmail] = React.useState("");
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<EmailInput
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
Input Validation
Let's get to the fun part, the input validation.
Before we see the code, let's talk how the validation works here.
useFormInput hook accepts from the given props the rules prop, which is an array of rules that will be applied upon validation step.
There about 10-15 pre-built validation rules that can be used directly so you won't have to write it in each project such as required to make sure the field is not empty, email to validate email pattern, minLength to validate the minimum length of the input field, pattern to validate the input value against certain pattern and so on.
Usually we define the rule props in the Component.defaultProps section as it rarely when you come across send custom validation for EmailInput for example rather than it is required and must be an email, unless so you can override it from the component call directly.
There are two ways of validating, validating on input change or validating on input blur, by the default the validation is on change, you can update this by passing validateOn change | blur prop.
Now when the input's value is changed, the validator will run all the given rules against the value, and it will stop validating at the first invalid rule against that value, so you can display just one error message.
Enough talking let's head back to code.
// EmailInput.tsx
import React from "react";
import { useFormInput } from "@mongez/react-form";
import { emailRule, requiredRule } from "@mongez/validator";
export default function EmailInput(props) {
const { name, error, id, type } = useFormInput(props);
console.log(error);
return <input type={type} name={name} />;
}
EmailInput.defaultProps = {
rules: [requiredRule, emailRule],
type: "email",
};
Here we defined our rules list, which are required and email rules, this will validate the input value each time the user types anything against these two rules.
But for the previous snippet, nothing much will happen as we didn't pass the onChange and value props to the input element.
// EmailInput.tsx
import React from "react";
import { useFormInput } from "@mongez/react-form";
import { emailRule, requiredRule } from "@mongez/validator";
export default function EmailInput(props) {
const { name, error, value, onChange } = useFormInput(props);
return <input type="email" value={value} onChange={onChange} name={name} />;
}
EmailInput.defaultProps = {
rules: [requiredRule, emailRule],
type: "email",
};
So far the only rule that will be applied is emailRule as it validates only if the user inputs some text.
Let's tell the validator to check for the input that should have a value.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { Form, FormInterface } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<EmailInput name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
We passed required prop, then the requiredRule now will work.
// EmailInput.tsx
import React from "react";
import { useFormInput } from "@mongez/react-form";
import { emailRule, requiredRule } from "@mongez/validator";
export default function EmailInput(props) {
const { name, error, value, onChange } = useFormInput(props);
console.log(error); // null for first render
return <input type="email" value={value} onChange={onChange} name={name} />;
}
EmailInput.defaultProps = {
rules: [requiredRule, emailRule],
type: "email",
};
Now when the user types anything, the error key will return A InputError either it is null or a RuleResponse instance which contains the error type and error message, just type anything and see the console.
Display error message
Now let's display our error message in the dom.
// EmailInput.tsx
import React from "react";
import { useFormInput } from "@mongez/react-form";
import { emailRule, requiredRule } from "@mongez/validator";
export default function EmailInput(props) {
const { name, error, value, onChange } = useFormInput(props);
return (
<>
<input type="email" value={value} onChange={onChange} name={name} />
{error && <span>{error.errorMessage}</span>}
</>
);
}
EmailInput.defaultProps = {
rules: [requiredRule, emailRule],
type: "email",
};
Manually validating component
You may also validate the component manually instead of using the rules.
// EmailInput.tsx
import React from "react";
import Is from "@mongez/supportive-is";
import { useFormInput } from "@mongez/react-form";
export default function EmailInput(props) {
const { name, setValue, value, error, setError } = useFormInput(props);
const onChange = (e) => {
const newValue = e.target.value;
if (Is.empty(newValue)) {
setError({
errorType: "required",
errorMessage: "This input is required",
});
} else if (!Is.email(newValue)) {
setError({
errorType: "email",
errorMessage: "Invalid Email Address",
});
} else {
setError(null);
}
setValue(newValue);
};
return (
<>
<input type="email" value={value} onChange={onChange} name={name} />
{error && <span>{error.errorMessage}</span>}
</>
);
}
In our previous example, we got introduced two new methods, setValue and setError, these methods are used to set the component value and error respectively.
setError function accepts null for no errors and RuleResponse from Mongez Validator for displaying an error.
It's recommended to use rules instead, this will make your code cleaner and easier to maintain.
The onError prop
Now you may detect if the component catches an error from the its own rules using onError
// EmailInput.tsx
import React from "react";
import { emailRule } from "@mongez/validator";
import { useFormInput } from "@mongez/react-form";
export default function EmailInput(props) {
const { name, value, error, onChange } = useFormInput(props);
return (
<>
<input type="email" value={value} onChange={onChange} name={name} />
{error && <span>{error.errorMessage}</span>}
</>
);
}
EmailInput.defaultProps = {
rules: [requiredRule, emailRule],
type: "email",
};
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
const onError = (error: RuleResponse, formInput: FormControl) => {
console.log(error); // will be triggered only if there is an error
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<EmailInput name="email" onError={onError} required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
The validate prop
The validate prop will allow you to manually validate the input.
This will override the
rulesprop as it will be totally ignored whenvalidateprop is passed
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import Is from "@mongez/supportive-is";
import { RuleResponse } from "@mongez/validator";
import {
Form,
FormInterface,
FormControl,
InputError,
} from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
const validateEmail = (formControl: FormControl): InputError => {
if (!formControl.value) {
return {
type: "required",
hasError: true,
errorMessage: "The email input is required",
} as RuleResponse;
} else {
if (!Is.email(formControl.value)) {
return {
type: "email",
hasError: true,
errorMessage: "Invalid Email Address",
} as RuleResponse;
}
}
// return null means the input is valid
return null;
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<EmailInput name="email" validate={validateEmail} required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
The validate callback must return null to ensure the input is valid, or a RuleResponse if the input is not valid.
When the validate prop returns a RuleResponse, it will be passed to onError as well.
Custom error messages
You can override the error messages that are being set by the rules list using errors prop.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form onSubmit={performLogin}>
<EmailInput
name="email"
errors={{
email: "This email is invalid",
required: "Email input can not be empty",
}}
required
/>
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
The errors props can receive an object to override error messages based on the rule response error type, or it can be used as a callback function for dynamic error messaging.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form onSubmit={performLogin}>
<EmailInput
name="email"
errors={(error: RuleResponse, formControl: FormControl) => {
if (error.type === "required") return "The email input is required";
if (error.type === "email") return "This email is invalid";
return "Some Other Error";
}}
required
/>
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
Validating on blur instead of on change
By default, the validation occurs on onChange prop, but you may set it on onBlur event instead using validateOn prop.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
const onError = (error: RuleResponse, formInput: FormControl) => {
console.log(error); // will be triggered only if there is an error
};
return (
<Form collectValuesFromDOM onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" onError={onError} required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
Accepted values:
change|blur, default ischange
Manually validate the form
We can also trigger form validation using form.validate() method.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const form = React.useRef();
React.useEffect(() => {
setTimeout(() => {
form.current.validate();
}, 2000);
}, []);
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form ref={form} collectValuesFromDOM onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
The previous example will trigger form validation after two seconds from component rendering.
Determine if form is valid
We can also use isValid() method to check if the form is valid or not.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const form = React.useRef();
React.useEffect(() => {
setTimeout(() => {
form.current.validate();
if (form.isValid()) {
alert('All Good, you can pass now!');
}
}, 2000);
}, []);
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form ref={form} collectValuesFromDOM onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
Validating only certain inputs
In some situations we need to validate only certain inputs, for example when working with form wizards or steppers, just pass an array of names to form.validate.
Please note this won't work with native DOM inputs as it must be registered in the form as form control.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const form = React.useRef();
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
const validateEmail = () => {
form.current.validate(["email"]);
};
return (
<Form ref={form} collectValuesFromDOM onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
<button type="button" onCLick={validateEmail}>
Validate Email Only{" "}
</button>
</Form>
);
}
Validate only visible elements
You may trigger form validation only for the visible form elements in the DOM, this can be useful if form elements are hidden under tabs or stepper but not removed from the DOM.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const form = React.useRef();
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
const validate = () => {
form.current.validateVisible(); // this will only validate the email input
// the password input will not be triggered for validation
};
return (
<Form ref={form} collectValuesFromDOM onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input
type="password"
style={{
display: "none",
}}
name="password"
placeholder="Password"
/>
<br />
<button>Login</button>
<button type="button" onCLick={validate}>
Validate Email Only{" "}
</button>
</Form>
);
}
Manually registering form control
We used useFormInput for handling many cases along with registering to the form, in some cases we might only need to register our component to the form without any additional helpers such as name dot notation or auto generating id if not passed.
// PasswordInput.tsx
import React from "react";
import { emailRule } from "@mongez/validator";
import { useForm } from "@mongez/react-form";
export default function PasswordInput({
defaultValue,
value,
onChange,
...otherProps
}) {
const [internalValue, setValue] = React.useState(value || defaultValue);
const formContext = useForm();
React.useEffect(() => {
const { form } = formContext;
form.register({
name: props.name,
value: internalValue,
id: props.id,
control: "input",
changeValue: (newValue) => {
setValue(newValue);
},
reset: () => {
setValue("");
},
});
}, []);
return (
<>
<input type="password" value={value} onChange={onChange} name={name} />
{error && <span>{error.errorMessage}</span>}
</>
);
}
Form Control Events
Every form control has several events that you can subscribe to when it occurs, here are the available events:
/**
* Form control events that can be subscribed to by the form control
*/
export type FormControlEvent =
| "change"
| "reset"
| "disabled"
| "unregister"
| "validation.start"
| "validation.success"
| "validation.error"
| "validation.end";
This is useful when you want to listen for input change from another input and vice versa.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const form = React.useRef();
const performLogin = (e: React.FormEvent, form: FormInterface) => {
// triggered from the useEffect hook
};
React.useEffect(() => {
const formControl = form.control("email");
const event = formControl.on(
"change",
(newValue: string, formControl: FormControl) => {
console.log(newValue); // will be triggered when the email input is changed
}
);
// Don't forget to unsubscribe when the component unmounts
return () => event.unsubscribe();
}, []);
return (
<Form ref={form} onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
Form Control Available Events
-
change: Triggered when value is changed, also when the form control value is reset, this event is triggered as well.
Please note the change event is triggered before the validation events
formControl.on("change", (newValue: string, formControl: FromControl) => {
//
});
-
reset: Triggered when value is reset, which will trigger thechangeevent as well.
formControl.on("reset", (formControl: FromControl) => {
//
});
The reset event is triggered after
changeevent.
-
validation.start: Triggered before validation starts.
formControl.on("validation.start", (formControl: FromControl) => {
//
});
-
validation.end: Triggered after validation ends.
formControl.on(
"validation.end",
(isValid: boolean, formControl: FromControl) => {
//
}
);
-
validation.success: Triggered when validation is valid.
formControl.on("validation.success", (formControl: FromControl) => {
//
});
-
validation.error: Triggered when validation is not valid.
formControl.on("validation.error", (formControl: FromControl) => {
//
});
-
validation.unregister: Triggered when form component is unmounted.
formControl.on("validation.unregister", (formControl: FromControl) => {
//
});
-
validation.disabled: Triggered when form disabled state is changed.
formControl.on('validation.disabled', (isDisabled: boolean formControl: FromControl) => {
//
});
Manually submitting form
Form can be submitted as well directly using form.submit method.
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const form = React.useRef();
const performLogin = (e: React.FormEvent, form: FormInterface) => {
// triggered from the useEffect hook
};
React.useEffect(() => {
setTimeout(() => {
form.current.submit();
}, 2000);
}, []);
return (
<Form ref={form} onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
Reset Form
To reset the form values, states, and changes, alongside with all registered form controls, we can use form.reset() method.
form.reset(formControlNames: string[]): FormControl[]
// LoginPage.tsx
import React from 'react';
import EmailInput from './EmailInput';
import { RuleResponse } from '@mongez/validator';
import { Form, FormInterface, FormControl } from '@mongez/react-form';
export default function LoginPage() {
const form = React.useRef();
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
const resetForm = () => {
form.current.reset();
}
return (
<Form ref={form} collectValuesFromDOM onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
<button type="button" onClick={resetForm}>Reset<button>
</Form>
)
}
Or event better, You may also use ResetFormButton component for shortage.
// LoginPage.tsx
import React from 'react';
import EmailInput from './EmailInput';
import { RuleResponse } from '@mongez/validator';
import { Form, ResetFormButton, FormInterface, FormControl } from '@mongez/react-form';
export default function LoginPage() {
const form = React.useRef();
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form ref={form} collectValuesFromDOM onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
<ResetFormButton>Reset<ResetFormButton>
</Form>
)
}
You may also set what inputs to be reset only by passing the input name to reset method.
const resetForm = () => {
form.current.reset(["email", "username"]);
};
If using ResetFormButton component then pass it as an array resetOnly
<ResetFormButton resetOnly={['email', 'username']}>Reset<ResetFormButton>
Disable Form elements
We can also disable all registered form inputs to be disabled.
form.disable(isDisabled: boolean, formControlNames: string[] = []): void
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
import { login } from "./../services/auth";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
form.disable(); // disable
// send ajax request
login(form.values())
.then((response) => {})
.catch((error) => {
console.log(error.response.data.error);
form.disable(false);
});
};
return (
<Form ref={form} collectValuesFromDOM onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
You may also use form.enable as an alias to form.disable(false).
Mark form elements as readOnly
This can be achieved using form.readOnly() method/
form.readOnly(isReadOnly: boolean = true, formControlNames: string[] = []): void
// LoginPage.tsx
import React from "react";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
import { login } from "./../services/auth";
export default function LoginPage() {
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
form.readOnly(); // all inputs are considered to be readOnly now
// send ajax request
login(form.values())
.then((response) => {})
.catch((error) => {
console.log(error.response.data.error);
form.readOnly(false);
});
};
return (
<Form ref={form} collectValuesFromDOM onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
Form Serializers
Another powered feature is that you can get form control values in variant ways using form serializers methods.
- form.values() Getting all values as object.
- form.toJSON() Getting all values as JSON.
- form.toQueryString() Getting all values as query string.
Getting all form values
We can get all form values either from registered form controls or from the dom directly using form.values(), it returns an object, the key is the form control name and the value is its corresponding value.
form.values(formControlNames: string[] = []): object
const formValues = form.values();
// or
const formValues = form.toObject();
Please keep in mind that if
collectValuesFromDOMprop is enabled, then the DOM input values will be merged with values coming from registered form controls.
form.toObject() is an alias to form.values()
Getting form values as query string
This method returns a string in a query string format using query-string package.
form.toQueryString(formControlNames: string[] = []): string
const formValues: string = form.toQueryString();
Serializing Only certain form controls
const formValues: string = form.toQueryString(["email", "password"]);
form.toString()is an alias to this method.
Getting form values as JSON
This can be done using toJSON method.
form.toJSON(formControlNames: string[] = []): string
const formValues: string = form.toJSON();
To get only json string to certain form controls, pass an array of form controls to the method.
const formValues: string = form.toJSON(["email", "password"]);
Getting form control
You may get a direct access to any registered form control either by form control name or by its id.
form.control(value: string, searchIn: "name" | "id" | "control" = "name"): FormControl | null
// getting the input by the name
const usernameInput: FormControl = form.control("username");
// or getting it by the id
const passwordInput: FormControl = form.control("password-id", "id");
If there is no matching value for that control, null will be returned instead.
Getting form controls list
We can get all registered form controls using form.controls()
const formControls = form.controls();
You may also getting controls for the given names only
const formControls = form.controls(["email", "password"]);
Control Modes And Control Types
Each form control has two main attributes, control and type.
All inputs regardless its type is considered to be a input control.
All button regardless its type is considered to be a button control.
type ControlMode = "input" | "button";
The input attribute value is a more specific, it can be one of the following types.
type ControlType =
| "text"
| "email"
| "checkbox"
| "radio"
| "number"
| "password"
| "hidden"
| "date"
| "time"
| "dateTime"
| "color"
| "range"
| "search"
| "tel"
| "url"
| "week"
| "select"
| "autocomplete"
| "file"
| "image"
| "button"
| "reset"
| "submit";
When registering new form, the control key must be provided and input key as well.
Defining form control mode and control type
Each registered form control has a control, by default it is input, you may assign the form control type yourself by setting control attribute in form control object.
useFormInputhook is registering the control type asinput, you may override it by passingcontrolkey in the passed props.
// PasswordInput.tsx
import React from "react";
import { emailRule } from "@mongez/validator";
import { useForm } from "@mongez/react-form";
export default function PasswordInput({
defaultValue,
value,
onChange,
...otherProps
}) {
const [internalValue, setValue] = React.useState(value || defaultValue);
const formContext = useForm();
React.useEffect(() => {
const { form } = formContext;
form.register({
name: props.name,
value: internalValue,
id: props.id,
control: "input",
type: "password",
changeValue: (newValue) => {
setValue(newValue);
},
reset: () => {
setValue("");
},
});
}, []);
return (
<>
<input type="password" value={value} onChange={onChange} name={name} />
{error && <span>{error.errorMessage}</span>}
</>
);
}
This can be useful to filter controls based on their types.
Getting controls based on its control type
To list all controls based on its type, use controlsOf method.
const inputControls = form.controlsOf("input");
To get only email inputs, pass second argument as the input type
const emailControls = form.controlsOf("input", "email");
You may also use another shorthand method form.inputs(type: ControlType): FormControl[]
const inputControls = form.inputs();
const emailControls = form.inputs("email");
Same as well with buttons
const buttons = form.buttons();
const submitButtons = form.buttons("submit");
Executing operation on form controls
We saw that we can get our controls all or part of list using form.controls, we can also perform an operation on controls directly using each method.
form.each(callback: (formControl: FormControl) => void, formControlNames: string[]): FormControl[]
form.each((formControl) => {
formControl.reset();
});
You may also do it on certain inputs by passing array of control names as second argument.
form.each(
(formControl) => {
formControl.reset();
},
["email", "password"]
);
More Form Hooks
Another useful hooks that can be used independently in your project.
Use input value hook
This hook is very simple, interacts as a React.useState hook but with a twist, it automatically detects the input value and update the state directly.
useInputValue<T>(initial: T): [value: T, setValue: React.SetStateAction<T>]
Before
import React from "react";
export default function MyComponent() {
const [value, setValue] = React.useState("");
const onChange = (e) => {
setValue(e.target.value);
};
return <input onChange={onChange} value={value} />;
}
After
import React from "react";
import { useInputValue } from "@mongez/react-form";
export default function MyComponent() {
const [value, setValue] = useInputValue("");
return <input onChange={setValue} value={value} />;
}
It can work with almost any onChange event either in the native input elements or components from UI Frameworks like Material UI, Semantic UI, Ant Design and so on.
Use Id Hook
The useId hook allows you to get a generated valid html id if the id prop is not passed.
import React from 'react';
import { useId } from '@mongez/react-form';
export default function MyComponent(props) {
const id = useId(props);
return (
<input {...props} id={id} />
)
}
<MyComponent /> // <input id="id-fw4Ar23" />
<MyComponent id="password-id" /> // <input id="password-id" />
Use Name Hook
The useName hook allows you to get convert a dot.notation name syntax to more standard name.
import React from 'react';
import { useName } from '@mongez/react-form';
export default function MyComponent(props) {
const name = useName(props);
return (
<input {...props} name={name} />
)
}
<MyComponent id="name" /> // <input name="name" />
<MyComponent id="name.first" /> // <input name="name[first]" />
Dirty Form
Whenever any form control's value is changed, the form control is marked as dirty and the whole form as well.
This could be useful if you want to get only the updated form inputs.
const { id, name, formInput } = useFormInput(props);
// check if form input is dirty
if (formInput.isDirty) {
// do something
}
Also, the form triggers a dirty event when any form input's value is changed.
Getting form control old value
Whenever any form control is marked as dirty, the oldValue key appears in the form control object as it stores the last value before current input value.
const { id, name, formInput } = useFormInput(props);
// check if form input is dirty
if (formInput.isDirty) {
console.log(formInput.oldValue);
}
Form Events
The form is shipped with multiple events types that can be listened to from.
// LoginPage.tsx
import React from "react";
import { EventSubscription } from "@mongez/events";
import EmailInput from "./EmailInput";
import { RuleResponse } from "@mongez/validator";
import { Form, FormInterface, FormControl } from "@mongez/react-form";
export default function LoginPage() {
const form = React.useRef();
React.useEffect(() => {
if (!form || !form.current) return;
const subscription: EventSubscription = form.current.on(
"validating",
() => {
// do something before form start validating on form submission
}
);
return () => subscription.unsubscribe();
}, []);
const performLogin = (e: React.FormEvent, form: FormInterface) => {
//
};
return (
<Form ref={form} collectValuesFromDOM onSubmit={performLogin}>
<EmailInput validateOn="blur" name="email" required />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Login</button>
</Form>
);
}
Here is the available list events
1- validating: Triggered before form validation.
form.on("validating", (formControlNames: string[], form) => {
// do something
});
2- validation: Triggered after form validation, the first argument is the form controls that have been validated.
Please note that this event is triggered after calling
onErrorif passed to the Form component.
form.on("validation", (validatedInputs: FormControl[], form) => {
// do something
});
3- disabling: Triggered before disabling/enabling form using form.disable()
form.on(
"disabling",
(
isDisabled: boolean,
oldDisabledState: boolean,
formControlNames: string[]
) => {
// do something
}
);
4- disable: Triggered after disabling/enabling form using form.disable()
form.on(
"disable",
(
isDisabled: boolean,
oldDisabledState: boolean,
formControls: FormControl[]
) => {
// do something
}
);
5- resetting: Triggered before resetting form using form.reset()
If the reset method is called without any arguments, then
formControlNameswill be an empty array.
form.on("resetting", (formControlNames: string[], form) => {
// do something
});
6- reset: Triggered after resetting form using form.reset()
If the reset method is called without any arguments, then
formControlswill be the entire registered form controls.
form.on("resetting", (formControls: FormControl[], form) => {
// do something
});
7- submitting: Triggered before form submission using either on normal form submission or using form.submit() method.
Please note that
submittingevent is triggered beforevalidatingevent.
form.on("submitting", (e: React.FormEvent, form) => {
// do something
});
8- submit: Triggered after form submission using either on normal form submission or using form.submit() method.
Please note that
submitevent is triggered only if form is valid otherwise it won't be triggered.
Thesubmitevent is triggered after callingonSubmiteither it is set or not.
form.on("submit", (e: React.FormEvent, form) => {
// do something
});
9- registering: Triggered before registering form input to the form.
form.on("registering", (formInput: FormControl, form) => {
// do something
});
10- register: Triggered after registering form input to the form.
form.on("register", (formInput: FormControl, form) => {
// do something
});
11- unregistering: Triggered before removing form input from the form.
form.on("unregistering", (formInput: FormControl, form) => {
// do something
});
12- unregister: Triggered after removing form input from the form.
form.on("unregister", (formInput: FormControl, form) => {
// do something
});
13- serializing: Triggered before form serializing.
Please note that it will be triggered twice if serializing is
toQueryStringortoJSON.
The type argument can be: object | queryString | json.
form.on("serializing", (type, formControlNames: string[], form) => {
// do something
});
14- serialize: Triggered after form serializing.
Please note that it will be triggered twice if serializing is
toQueryStringortoJSON.
The type argument can be: object | queryString | json.
form.on("serialize", (type, values, formControlNames: string[], form) => {
// do something
});
15- invalidControl: Triggered when form control is validated and being not valid.
form.on("invalidControl", (formControl: FormControl, form: FormInterface) => {
// do something
});
16- validControl: Triggered when form control is validated and being valid.
form.on("validControl", (formControl: FormControl, form: FormInterface) => {
// do something
});
17- invalidControls: Triggered when at least one form control is not valid.
form.on(
"invalidControls",
(formControls: FormControl[], form: FormInterface) => {
// do something
}
);
18- validControl: Triggered when all form controls are valid.
form.on("validControls", (formControls: FormControl[], form: FormInterface) => {
// do something
});
19- dirty: Triggered when at least one form inputs value has been changed
form.on(
"dirty",
(isDirty: boolean, dirtyControls: FormControl[], form: FormInterface) => {
// do something
}
);
Please note that the
dirtyevent is triggered also when the form is reset as it will be triggered afterresettingevent directly.
20- change: Triggered when form control's value has been changed.
form.on("change", (formControl: FormControl, form: FormInterface) => {
// do something
});
Please note that the
dirtyevent is triggered also when the form is reset as it will be triggered afterresettingevent directly.
Use Form Event Hook
Alternatively, you may use useFormEvent hook as it works seamlessly inside React Components.
import { useState } from "react";
import { useFormEvent } from "@mongez/react-form";
export default function LoginButton() {
const [isDisabled, setDisabled] = useState(false);
// if the form controls contain any invalid control, then disable the submit button
useFormEvent("invalidControls", () => setDisabled(true));
// if all form controls ar valid, then enable the submit button
useFormEvent("validControls", () => setDisabled(false));
// Enable/Disable the button on form submission
useFormEvent("submit", (isSubmitted: boolean) => setDisabled(isSubmitted));
// or in easier way
useFormEvent("submit", setDisabled);
// If form is being disabled
useFormEvent("disable", setDisabled);
return <button disabled={isDisabled}>Login</button>;
}
All registered events in useFormEvent are being unsubscribed once the component is unmounted.
Active Forms
All forms are being tracked using the activeForms utilities, which means you can get the current active form from anywhere in the project using getActiveForm utility.
import { getActiveForm } from "@mongez/react-form";
console.log(getActiveForm()); // null by default
By default the active form will be null until there is a form is mounted in the DOM, once there is a Form rendered you can get access to that form using the getActiveForm function.
import { getActiveForm } from "@mongez/react-form";
export default function LoginPage() {
React.useEffect(() => {
console.log(getActiveForm()); // will get the Form Component Which implements FormInterface
}, []);
return (
<>
<LoginFormComponent />
</>
);
}
Sometimes we may open multiple forms in one page, for example a single page that displays the login form and the register form, we can access any form of them using the getForm utility by passing the form id to it.
import { Form, getForm } from "@mongez/react-form";
export default function LoginPage() {
React.useEffect(() => {
console.log(getForm('login-form')); // Returns The FormInterface for the login-form
}, []);
return (
<>
<Form id="login-form">
//
</Form>
</>
);
}
Conclusion
I know the article is a bit too long, but it covers numerous features that will make your code easier and neat as well.
I hope you enjoyed the package, any feedback is so welcomed.
Have a nice day and happy coding :)
Top comments (0)