DEV Community

Hasan Zohdy
Hasan Zohdy

Posted on • Updated on

Mongez React Form, Powerful form handler for React Js

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);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
// 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} />;
}
Enter fullscreen mode Exit fullscreen mode

Please note that if there is no Form component in the parent tree, then FormContext will 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} />;
}
Enter fullscreen mode Exit fullscreen mode

Please note that if there is no Form component in the parent tree, then useForm will 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} />;
}
Enter fullscreen mode Exit fullscreen mode
// 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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", 
};
Enter fullscreen mode Exit fullscreen mode

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", 
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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", 
};
Enter fullscreen mode Exit fullscreen mode

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", 
};
Enter fullscreen mode Exit fullscreen mode

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>}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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", 
};
Enter fullscreen mode Exit fullscreen mode
// 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

The validate prop

The validate prop will allow you to manually validate the input.

This will override the rules prop as it will be totally ignored when validate prop 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Accepted values: change | blur, default is change

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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) => {
  //
});
Enter fullscreen mode Exit fullscreen mode
  • reset: Triggered when value is reset, which will trigger the change event as well.
formControl.on("reset", (formControl: FromControl) => {
  //
});
Enter fullscreen mode Exit fullscreen mode

The reset event is triggered after change event.

  • validation.start: Triggered before validation starts.
formControl.on("validation.start", (formControl: FromControl) => {
  //
});
Enter fullscreen mode Exit fullscreen mode
  • validation.end: Triggered after validation ends.
formControl.on(
  "validation.end",
  (isValid: boolean, formControl: FromControl) => {
    //
  }
);
Enter fullscreen mode Exit fullscreen mode
  • validation.success: Triggered when validation is valid.
formControl.on("validation.success", (formControl: FromControl) => {
  //
});
Enter fullscreen mode Exit fullscreen mode
  • validation.error: Triggered when validation is not valid.
formControl.on("validation.error", (formControl: FromControl) => {
  //
});
Enter fullscreen mode Exit fullscreen mode
  • validation.unregister: Triggered when form component is unmounted.
formControl.on("validation.unregister", (formControl: FromControl) => {
  //
});
Enter fullscreen mode Exit fullscreen mode
  • validation.disabled: Triggered when form disabled state is changed.
formControl.on('validation.disabled', (isDisabled: boolean formControl: FromControl) => {
  //
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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"]);
};
Enter fullscreen mode Exit fullscreen mode

If using ResetFormButton component then pass it as an array resetOnly


<ResetFormButton resetOnly={['email', 'username']}>Reset<ResetFormButton>
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Form Serializers

Another powered feature is that you can get form control values in variant ways using form serializers methods.

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();
Enter fullscreen mode Exit fullscreen mode

Please keep in mind that if collectValuesFromDOM prop 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();
Enter fullscreen mode Exit fullscreen mode

Serializing Only certain form controls

const formValues: string = form.toQueryString(["email", "password"]);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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"]);
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

You may also getting controls for the given names only

const formControls = form.controls(["email", "password"]);
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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.

useFormInput hook is registering the control type as input, you may override it by passing control key 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>}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

To get only email inputs, pass second argument as the input type

const emailControls = form.controlsOf("input", "email");
Enter fullscreen mode Exit fullscreen mode

You may also use another shorthand method form.inputs(type: ControlType): FormControl[]

const inputControls = form.inputs();
const emailControls = form.inputs("email");
Enter fullscreen mode Exit fullscreen mode

Same as well with buttons

const buttons = form.buttons();
const submitButtons = form.buttons("submit");
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

You may also do it on certain inputs by passing array of control names as second argument.

form.each(
  (formControl) => {
    formControl.reset();
  },
  ["email", "password"]
);
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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]" />
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here is the available list events

1- validating: Triggered before form validation.

form.on("validating", (formControlNames: string[], form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

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 onError if passed to the Form component.

form.on("validation", (validatedInputs: FormControl[], form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

3- disabling: Triggered before disabling/enabling form using form.disable()

form.on(
  "disabling",
  (
    isDisabled: boolean,
    oldDisabledState: boolean,
    formControlNames: string[]
  ) => {
    // do something
  }
);
Enter fullscreen mode Exit fullscreen mode

4- disable: Triggered after disabling/enabling form using form.disable()

form.on(
  "disable",
  (
    isDisabled: boolean,
    oldDisabledState: boolean,
    formControls: FormControl[]
  ) => {
    // do something
  }
);
Enter fullscreen mode Exit fullscreen mode

5- resetting: Triggered before resetting form using form.reset()

If the reset method is called without any arguments, then formControlNames will be an empty array.

form.on("resetting", (formControlNames: string[], form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

6- reset: Triggered after resetting form using form.reset()

If the reset method is called without any arguments, then formControls will be the entire registered form controls.

form.on("resetting", (formControls: FormControl[], form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

7- submitting: Triggered before form submission using either on normal form submission or using form.submit() method.

Please note that submitting event is triggered before validating event.

form.on("submitting", (e: React.FormEvent, form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

8- submit: Triggered after form submission using either on normal form submission or using form.submit() method.

Please note that submit event is triggered only if form is valid otherwise it won't be triggered.
The submit event is triggered after calling onSubmit either it is set or not.

form.on("submit", (e: React.FormEvent, form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

9- registering: Triggered before registering form input to the form.

form.on("registering", (formInput: FormControl, form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

10- register: Triggered after registering form input to the form.

form.on("register", (formInput: FormControl, form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

11- unregistering: Triggered before removing form input from the form.

form.on("unregistering", (formInput: FormControl, form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

12- unregister: Triggered after removing form input from the form.

form.on("unregister", (formInput: FormControl, form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

13- serializing: Triggered before form serializing.

Please note that it will be triggered twice if serializing is toQueryString or toJSON.

The type argument can be: object | queryString | json.

form.on("serializing", (type, formControlNames: string[], form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

14- serialize: Triggered after form serializing.

Please note that it will be triggered twice if serializing is toQueryString or toJSON.

The type argument can be: object | queryString | json.

form.on("serialize", (type, values, formControlNames: string[], form) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

15- invalidControl: Triggered when form control is validated and being not valid.

form.on("invalidControl", (formControl: FormControl, form: FormInterface) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

16- validControl: Triggered when form control is validated and being valid.

form.on("validControl", (formControl: FormControl, form: FormInterface) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

17- invalidControls: Triggered when at least one form control is not valid.

form.on(
  "invalidControls",
  (formControls: FormControl[], form: FormInterface) => {
    // do something
  }
);
Enter fullscreen mode Exit fullscreen mode

18- validControl: Triggered when all form controls are valid.

form.on("validControls", (formControls: FormControl[], form: FormInterface) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

19- dirty: Triggered when at least one form inputs value has been changed

form.on(
  "dirty",
  (isDirty: boolean, dirtyControls: FormControl[], form: FormInterface) => {
    // do something
  }
);
Enter fullscreen mode Exit fullscreen mode

Please note that the dirty event is triggered also when the form is reset as it will be triggered after resetting event directly.

20- change: Triggered when form control's value has been changed.

form.on("change", (formControl: FormControl, form: FormInterface) => {
  // do something
});
Enter fullscreen mode Exit fullscreen mode

Please note that the dirty event is triggered also when the form is reset as it will be triggered after resetting event 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>;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)