February 27, 2023

React Dynamic Form Using Formik's FieldArray Component

In this post you'll see how to create a React form with dynamic array field using Formik and Yup for object schema validation.

Many a times you need a form where you give user a functionality to add the same set of fields again and again which means giving an input or group of inputs as a dynamic array. For example, suppose you have a student form where you want to give a button to add qualification and whenever user clicks that button a new set of input fields for qualification should be added to the form.

Formik FieldArray

Dynamic React form using Formik's FieldArray

In Formik, which is a third party lightweight React form library, there is a <FieldArray> component that helps with common array/list manipulations. Using index of the array you can create input groups and do validations.

Formik's FieldArray example

In the example form we'll capture student details so the form fields are Student Name, Student Age and Qualification which is an array having two fields name and year. For the qualification field you want to provide the functionality that student should be able to add more than one qualification.

In this post we'll concentrate more on FieldArray, to get more details about Formik and Yup refer this post- React Form + Formik + Yup Validation Example

import { ErrorMessage, Field, FieldArray, Form, Formik, getIn } from "formik"
import * as Yup from 'yup';
import './form.css';
const StudentForm = () => {
  return (
    <Formik
      initialValues={{
        studentName: '',
        studentAge: '',
        qualification: [
          {
            name: '',
            year: ''
          }
        ]

      }}
      validationSchema={Yup.object({
        studentName: Yup.string()
            .max(20, 'Must be 20 characters or less')
            .required('Required'),
        studentAge: Yup.number()
            .min(18, 'Age must not be less than 18')
            .max(22, 'Age must not be greater than 22')
            .required('Required'),
        qualification: Yup.array().of(
            Yup.object().shape({
                name: Yup.string()
                    .required('Required'),
                year: Yup.number()
                    .required('Required'),
            })
        )
      })}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
            console.log(JSON.stringify(values, null, 2));
            setSubmitting(false);
        }, 400);
      }}

    >
    {({ errors, touched, values }) => (
      <Form>
          <h3>Student Form</h3>
          <div className="row">
              <div className="form-group col-sm-4 mb-2" >
                <label htmlFor="studentName" className="form-label">Student Name</label>
                <Field type="text" name="studentName" placeholder="Enter studentName"
                    className={'form-control' + (errors.studentName && touched.studentName ? ' is-invalid' : '')} />
                <ErrorMessage name="studentName" component="div" className="invalid-feedback" />
              </div>
              <div className="form-group col-sm-4 mb-2" >
                <label htmlFor="studentAge" className="form-label">Student Age</label>
                <Field type="text" name="studentAge" placeholder="Enter age"
                    className={'form-control' + (errors.studentAge && touched.studentAge ? ' is-invalid' : '')} />
                <ErrorMessage name="studentAge" component="div" className="invalid-feedback" />
              </div>
          </div>

          <FieldArray name="qualification">
            {({ remove, push }) => (

              <div>
                  <h4>Add qualifications</h4>
                  <button
                      type="button"
                      className="btn btn-secondary mb-2"
                      onClick={() => push({ name: '', year: '' })}
                  >
                      Add Qualification
                  </button>
                  {values.qualification.length > 0 &&
                    values.qualification.map((qual, index) => (

                    <div className="row" key={index}>
                      <div className="col-sm-4">
                        <label htmlFor={`qualification.${index}.name`} className="form-label">Name</label>
                        <Field
                            name={`qualification.${index}.name`}
                            placeholder="qualification"
                            type="text"
                            className={'form-control' + (getIn(errors, `qualification.${index}.name`) && getIn(touched, `qualification.${index}.name`) ? ' is-invalid' : '')}
                        />
                        <ErrorMessage
                            name={`qualification.${index}.name`}
                            component="div"
                            className="invalid-feedback"
                        />
                      </div>
                      <div className="col-sm-4">
                        <label htmlFor={`qualification.${index}.year`} className="form-label">Year</label>
                        <Field
                            name={`qualification.${index}.year`}
                            placeholder="year"
                            type="number"
                            className={'form-control' + (getIn(errors, `qualification.${index}.year`) && getIn(touched, `qualification.${index}.year`) ? ' is-invalid' : '')}
                        />
                        <ErrorMessage
                            name={`qualification.${index}.year`}
                            component="div"
                            className="invalid-feedback"
                        />
                      </div>
                      <div className="col-sm-1">
                        <label className="form-label">&nbsp;</label>
                        <button
                            type="button"
                            className="btn btn-danger btn-as-block"
                            onClick={() => remove(index)}
                        >
                            X
                        </button>
                      </div>
                  </div>
                ))}

              </div>
            )}
          </FieldArray>
          <button type="submit" className="btn btn-primary">Submit</button>
        </Form>
      )}

    </Formik>
  );
}

export default StudentForm;

There is also a small CSS code for aligning the delete button.

Form.css

.btn.btn-as-block {
  display: block;
}

Some important points to note here-

  1. In the example Bootstrap 5 is used for styling. You can import Bootstrap by adding following to the <head> section of the index.html file.
    <link rel="stylesheet" href=https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous" />
    	
  2. Initial values for the form fields are as empty fields. You can see that qualification is defined as an array of objects with two properties- name and year.
    qualification: [
      {
        name: '',
        year: ''
      }
    ]
       
  3. In the validation Yup.array() function is used to provide validation for the fields in qualification array.
  4. The submission function (onSubmit) logs the form values.
  5. In the <Form> section some of the Bootstrap classes like form-group, form-label, form-control, is-invalid, invalid-feedback are used.
  6. If there is an error then apart from form-control, is-invalid class is also added.
  7. For adding qualification, a button "Add Qualification" is provided. On clicking it a new object with two fields- name, year is pushed into an array.
    <button
      type="button"
      className="btn btn-secondary mb-2"
      onClick={() => push({ name: '', year: '' })}>
        Add Qualification
    </button>
    
  8. To give unique name to each group of fields in the array index is used
    name={`qualification.${index}.year`}
    
  9. getIn, which is a utility function included in Formik is used to get errors for the specific index.
  10. With each array index another button to remove that index is also added.
    	<button
      type="button"
      className="btn btn-danger"
      onClick={() => remove(index)}>
        X
    </button>
    

With field validation error messages

React dynamic form validation

That's all for the topic React Dynamic Form Using Formik's FieldArray Component. If something is missing or you have something to share about the topic please write a comment.


You may also like

February 10, 2023

React Form + Formik + Yup Validation Example

In the post React Form Using Formik's useFormik() Hook we have seen how to create a form in React using Formik library and how to do validation. In that post we used useFormik() hook and did the validation ourselves. Formik provides integration with Yup for object schema validation.

Formik has a special configuration prop for Yup called validationSchema which will automatically transform Yup's validation errors messages into an object whose keys match values/initialValues/touched so that it is easy to map error message with the corresponding form field.

Installing Yup

you can install Yup from NPM or yarn

With npm

npm install yup -save

With yarn

yarn add yup

Using Formik component

To further reduce code and make it more readable Formik comes with components like <Formik />, <Form />, <Field />, and <ErrorMessage />. These formik components use React Context implicitly.

React form example with Formik and Yup

In this example we'll create a form with 5 fields- First Name, Last Name, Join Date, Email and Employment Type.

You can swap useFormik() hook with Formik component, with that you can pass initial values for the formFields, validation schema and a submission function.

<Formik 
  initialValues= {{
      firstName: '',
      lastName: '',
      joinDate: '',
      email: '',
      employmentType: 'permanent'
  }}
  validationSchema={Yup.object({
      firstName: Yup.string()
      .max(10, 'Must be 10 characters or less')
      .required('Required'),
      lastName: Yup.string()
      .max(15, 'Must be 15 characters or less')
      .required('Required'),
      joinDate:  Yup.date().max(new Date(), "Join date can't be greater than current date").required('Required'),
      email: Yup.string().email('Invalid email address').required('Required'),
  })}
  onSubmit={(values, { setSubmitting }) => {
      setTimeout(() => {
      console.log(JSON.stringify(values, null, 2));
      setSubmitting(false);
      }, 400);
  }}
>

By using Field component you don't need to pass event handlers- onChange, onBlur, value explicitly.

<Field type="text" name="firstName" />

Here is the full React form example using Formik, Field, Form, ErrorMessage components and Yup for validation.

import { ErrorMessage, Field, Form, Formik } from "formik";
import * as Yup from 'yup';
const EmployeeInfoForm = () => {
  return(
    <Formik 
      initialValues= {{
          firstName: '',
          lastName: '',
          joinDate: '',
          email: '',
          employmentType: 'permanent'
      }}
      validationSchema={Yup.object({
          firstName: Yup.string()
          .max(10, 'Must be 10 characters or less')
          .required('Required'),
          lastName: Yup.string()
          .max(15, 'Must be 15 characters or less')
          .required('Required'),
          joinDate:  Yup.date().max(new Date(), "Join date can't be greater than current date").required('Required'),
          email: Yup.string().email('Invalid email address').required('Required'),
      })}
      onSubmit={(values, { setSubmitting }) => {
          setTimeout(() => {
          console.log(JSON.stringify(values, null, 2));
          setSubmitting(false);
          }, 400);
      }}
    >
    {({ errors, touched }) => (
      <Form>

        <div className="form-group col-sm-4 mb-2" >
            <label htmlFor="firstName" className="form-label">First Name</label>
            <Field type="text" name="firstName" placeholder="Enter firstname" 
            className={'form-control' + (errors.firstName && touched.firstName ? ' is-invalid' : '')} />
            <ErrorMessage name="firstName" component="div" className="invalid-feedback" />       
        </div>
        <div className="form-group col-sm-4 mb-2" >
            <label htmlFor="lastName" className="form-label">Last Name</label>
            <Field type="text" name="lastName" className={'form-control'+ (errors.lastName && touched.lastName ? ' is-invalid' : '')}/>
            <ErrorMessage name="lastName" component="div" className="invalid-feedback"/>  
        </div>
        <div className="form-group col-sm-4 mb-2" >
            <label htmlFor="joinDate" className="form-label">Join Date</label>
            <Field type="date" name="joinDate" className={'form-control'+ (errors.joinDate && touched.joinDate ? ' is-invalid' : '')}/>
            <ErrorMessage name="joinDate" component="div" className="invalid-feedback"/>  
        </div>
        <div className="form-group col-sm-4 mb-2" >
            <label htmlFor="email" className="form-label">Email</label>
            <Field type="email" name="email" className={'form-control'+ (errors.email && touched.email ? ' is-invalid' : '')}/>
            <ErrorMessage name="email" component="div" className="invalid-feedback"/>  
        </div>
        <div className="form-group col-sm-4 mb-2" >
            <label htmlFor="employmentType" className="form-label">Employment Type:</label>
            <Field as="select" name="employmentType" className="form-select " >              
                <option value="permanent">Permanent</option>
                <option value="contract">Contract</option>
            </Field>
        </div>
          <div className="form-group col-sm-4 mb-2" >
              <button type="submit" className="btn btn-primary">Submit</button>
          </div>          
      </Form>
     )}
    </Formik>
  );
}

export default EmployeeInfoForm;

Some important points to note here-

  1. In the example Bootstrap 5 is used for styling. You can import Bootstrap by adding following to the <head> section of the index.html file.
    <link rel="stylesheet" href=https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous" />
    
  2. Initial values for the form fields are as empty fields except for the employmentType which is a drop-down list where pre-selected option is given.
  3. Validation is done using Yup which is quite self-explanatory. For join date it should not go beyond the current date.
  4. The submission function (onSubmit) logs the form values.
  5. In the <Form> section some of the Bootstrap classes like form-group, form-label, form-control, is-invalid, invalid-feedback are used.
  6. If there is an error then apart from form-control, is-invalid class is also added.
  7. The <Field> component by default will render an <input> component. <Field> also accepts a few other props to let you render other elements like textarea, select.
    <Field name="message" as="textarea" />
    
    <Field name="employmentType" as="select"  />  
    

With Error Messages

React form with Formik

With All Form Values

React form Formik and Yup

That's all for the topic React Form + Formik + Yup Validation Example. If something is missing or you have something to share about the topic please write a comment.


You may also like

February 8, 2023

React Form Using Formik's useFormik() Hook

In the post React Form Creation and Validation Using React Hooks we have seen how to create a form using React and how to do validation. As you can notice the whole process of creating form in React is quite verbose if you do everything on your own.

You need to have a lot of repeated code for-

  1. Managing form fields states.
  2. Validation and when to show/not show error messages.
  3. Handling form submission

Formik, a third-party lightweight React form library addresses the above mentioned issues. Formik standardizes the creation of from fields and the form submission process.

Formik installation

You can install Formik with Yarn or NPM

With Yarn

yarn add formik

With npm

npm install formik -save

Formik React form example

In this example we'll create a form with 5 fields- First Name, Last Name, Join Date, Email and Employment Type. This example uses useFormik() which is a custom React hook.

Though using useFormik() hook is quite verbose and you can create a form using Formik in a more readable way using <Formik> component and using Yup for form validation. Check the post here- React Form + Formik + Yup Validation Example

Using useFormik() hook you can define the form fields, how to validate and form submission process at the same place.

const formik = useFormik({
  initialValues: {
    firstName: '',
    lastName: '',
    joinDate: '',
    email: '',
    employmentType: 'permanent'
  },
  validate,
  onSubmit: values => {
    alert(JSON.stringify(values, null, 2));
  },
});

As you can see form's initial values, a submission function (inSubmit) and a validation function (validate) is passed to the useFormik() hook.

Event handling

useFormik() hook returns form state and helper methods which can be used out of the box. Some of the helper methods you will use in your form are-

  • handleSubmit: A submission handler
  • handleChange: A change handler to pass to each <input>, <select>, or <textarea>
  • handleBlur: To keep track of which fields have been visited, stores this information in an object called touched which can store boolean values true or false
  • values: Our form’s current values

That's how you'll use handleSubmit

<form onSubmit={formik.handleSubmit}>
Using handleChange, handleBlur and value.
<input 
  type="text" 
  id="firstName" 
  name="firstName"
  onChange={formik.handleChange} 
  onBlur={formik.handleBlur}
  value={formik.values.firstName}>
</input>

With that Formik will take care of managing form fields state and event handling. You don't need to write custom event handlers for every single input.

Validation and error messages

Formik also keeps track of validation and error messages. You can specify a custom validation function and pass it as validate to the useFormik() hook. If an error exists, this custom validation function should produce an error object with properties matching the form values/initialValues.

You would generally show error once you come out of a form field. For that Formik has handleBlur() event handler to keep track of which fields have been visited.

That's how you'll write custom validate function.

const validate = values => {
    const errors = {};
    if (!values.firstName) {
        errors.firstName = 'Required';
    } else if (values.firstName.length > 10) {
        errors.firstName = 'Must be 10 characters or less';
    }
    // Validation for other fields

That's how you'll check if field visited and if field has error to display error message.

{formik.touched.firstName && formik.errors.firstName ? <div>{formik.errors.firstName}</div> : null}

Here is the full React form example created using useFormik() hook of Formik.

import { useFormik } from "formik"
import './form.css';
const validate = values => {
    const errors = {};
    if (!values.firstName) {
        errors.firstName = 'Required';
    } else if (values.firstName.length > 10) {
        errors.firstName = 'Must be 10 characters or less';
    }
    if (!values.lastName) {
        errors.lastName = 'Required';
    } else if (values.lastName.length > 15) {
        errors.lastName = 'Must be 15 characters or less';
    }
    if (!values.joinDate) {
        errors.joinDate = 'Required';
    }
    if (!values.email) {
        errors.email = 'Required';
    } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
        errors.email = 'Invalid email address';
    }
    return errors;
}
const EmployeeForm = () => {
    const formik = useFormik({
        initialValues: {
            firstName: '',
            lastName: '',
            joinDate: '',
            email: '',
            employmentType: 'permanent'
        },
        validate,
        onSubmit: values => {
            console.log(JSON.stringify(values, null, 2));
        },
    });
    return (
        <form onSubmit={formik.handleSubmit}>
            <div>
            <label htmlFor="firstName">First Name</label>
            <input type="text" id="firstName" name="firstName"
                onChange={formik.handleChange} onBlur={formik.handleBlur}
                value={formik.values.firstName}></input>
            {formik.touched.firstName && formik.errors.firstName ? <div className="error">{formik.errors.firstName}</div> : null}
            </div>
            <div>
            <label htmlFor="lastName">Last Name</label>
            <input type="text" id="lastName" name="lastName"
                onChange={formik.handleChange} onBlur={formik.handleBlur}
                value={formik.values.lastName}></input>
             {formik.touched.lastName && formik.errors.lastName ? <div className="error">{formik.errors.lastName}</div> : null}        
            </div>
            <div>
            <label htmlFor="joinDate">Join Date</label>
            <input type="date" id="joinDate" name="joinDate"
                onChange={formik.handleChange} onBlur={formik.handleBlur}
                value={formik.values.joinDate}></input>      
             {formik.touched.joinDate && formik.errors.joinDate ? <div className="error">{formik.errors.joinDate}</div> : null}      
            </div>
            <div>
            <label htmlFor="email">Email</label>
            <input type="email" id="email" name="email"
                onChange={formik.handleChange} onBlur={formik.handleBlur}
                value={formik.values.email}></input>
            {formik.touched.email && formik.errors.email ? <div className="error">{formik.errors.email}</div> : null}    
            </div>
            <div>
            <label htmlFor="employmentType">Employment Type:</label>
                <select id="employmentType" name="employmentType" 
                  value={formik.values.employmentType} onChange={formik.handleChange} 
                  onBlur={formik.onBlur}>
                    <option value="permanent">Permanent</option>
                    <option value="contract">Contract</option>
                </select>
            </div>
            <button type="submit" disabled={!(formik.dirty && formik.isValid)}>Submit</button>
        </form>
    )
}
export default EmployeeForm;

To keep focus on the form styling is not used, only styling is for error message.

form.css

.error {
  color: red;
}

Some important points to note here-

  1. In the useFormik() hook, initialValues for the form fields are passed, all but one as empty fields, for the employmentType which is a drop-down list pre-selected option is there.
  2. A custom validation function is written to validate the form fields and create error messages. Reference for that function is also passed to the hook.
  3. onSubmit function just logs the form values.
  4. With form fields the same event handler functions (handleChange, handleBlur) are used, which is one of the benefit of using Formik library.
  5. Form values are passed to the validate function and the validation based on the requirement is done for the fields there. Error messages for the fields are also created.
  6. Formik keeps track of the visited fields and based on whether the field is touched or not and if it fails validation, error message for the field is displayed.

That's all for the topic React Form Using Formik's useFormik() Hook. If something is missing or you have something to share about the topic please write a comment.


You may also like