DEV Community

devneagu
devneagu

Posted on

[Front-end] : How to write complex Forms in less than 5 minutes via Formik & Yup

Build forms in React, without the tears

Formik helps you with :

  • input validation
  • formatting
  • error handling

We'll discuss the following subjects :

  • Validation Schema
  • Formik Wrapper
  • useFormikContext Hook
  • How to generate Validation Schema and Initial Values dynamically.
  • Show Errors on touch inside your custom field.
  • Show Internalization Error Messages depending on your schema.
  • How I used Formik for my project needs.

Formik wraps your form inside a context, inside the multiple context that it has, it will validate your schema, update your fields and keep your state.

Custom OnSubmit React Hook

I've created a custom hook, here you can add your event handlers in case you have multiple forms on your website, keeping the onSubmit logic separated by the UI Component.

    export default function useFormHandlers(){
        const contactOnSubmit = useCallback(async (values) => {
            // do something here
            // await (POST) 
        },[])

        return {
            contactOnSubmit
        }
    }
Enter fullscreen mode Exit fullscreen mode

Formik Wrapper UI aka the Parent

We imported useFormHandlers, we'll use it inside Formik OnSubmit function to send the values. In case you want to create custom styling inside the wrapper, you can use isSubmitting.

    const eventHandler = useFormHandlers();

    return (
        <Formik
            initialValues={initialValues}
            schemaValidation={schemaValidation}
            onSubmit={async (values, { setSubmitting }) => {
                await eventHandler.contactOnSubmit.bind(this,values);
            }}
        >
            {({
isSubmitting , handleSubmit,
        }) => (
                <form onSubmit={handleSubmit} className="bg-white">
                    {/* Fields */}
                    {/* Custom Fields */}
                </form>
        )}
        </Formik>
    )
Enter fullscreen mode Exit fullscreen mode

Formik Fields UI aka the Child

Formik does an excelent job at keeping your state according to the input name field. The "name" is the principal tag of keeping the state and obtaining the context.

Formik Field

<Field type="text" name="username" placeholder="Username" />
Enter fullscreen mode Exit fullscreen mode

Custom Formik Field

First, we'll want to create a new component, I have provided below how to set the field value of the formik value. You can see the whole list from "useFormikContext" Formik Docs. There are multiple hooks there from submitCount, errors object, fields props and metas and so on.

import { useField, useFormikContext } from "formik";
import { useCallback } from "react";

const CustomInput = ({ name, ...props }) => {
  const [field] = useField(name);
  // Get the full Formik Context using useFormikContext
  const formikCtx = useFormikContext();

  const eventHandler = useCallback((value) => {
    formikCtx.setFieldValue(name,value)
  },[])

  return (
    <>
        <input
            type="text"
            name={name}
            value={field.value}
            onChange={eventHandler.bind(this)}
            disabled={formikCtx.isSubmitting}
        />
        {/* In case we have schema validation, we can provide the error message. This works similar for the Formik Fields. */}
        {
            formikCtx.errors[name] && formikCtx.touched[name] && 
            <p>{formikCtx.errors[name]}</p>
        }
    </>
  );
};

export default CustomInput;
Enter fullscreen mode Exit fullscreen mode

Import the custom field input from your parent and voila! Now you have a custom input inside the formik.

<CustomInput name="username" />
Enter fullscreen mode Exit fullscreen mode

Schema Validation with Internalization via Yup

For my project, I had an object obtained from CMS with the following structure :

CMSFields :

[
    {
        "id" : 1,
        "label" : "Username",
        "required" : true,
        "minCharacters" : 4,
        "maxCharacters" : 16,
        "type" : "text",
        "defaultValue" : null,
        "name" :  "username",
        "placeholder" : "formikrocks"
    },
    {
        "id" : 2,
        "label" : "Email Address",
        "required" : true,
        "minCharacters" : 6,
        "maxCharacters" : 32,
        "type" : "email",
        "defaultValue" : null,
        "name" :  "email",
        "placeholder" : "formikrocks@formik.com"
    }
]
Enter fullscreen mode Exit fullscreen mode

With the following translations:

{
    "username" : {
        "en" : "This username is required.",
        "fr" : "Ce nom d'utilisateur est requis."
    },
    "minimumFieldLength" : {
        "en" : "Must be __VALUE__ characters or more",
    },
    "maximumFieldLength" : {
        "en" : "Must be __VALUE__ characters or less",
    },
    "fieldMatch" : {
        "en" : "__VALUE__ must match",
    },
    "default" : {
        "en" : "This field is required.",
        "fr" : "Ce champ est required."
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the next thing is to parse the CMSfield JSON using the following function :

import * as Yup from "yup";
import translationErrors from "../translationErrors";

export default function generateSchemaValidation(fields, locale) {
  const schema = {};


  const getErrorMessage = (nameRule,replaceValue = null) => {
    let message = '';
    try {
        message = translationErrors[nameRule][locale]
        if(replaceValue) message = message.replace('__VALUE__',replaceValue);
    } catch(err) {
        message = translationErrors?.[default]?.[locale]
    }
    return message;
  }

  fields?.forEach((field) => {
    let name = field.name;
    if (field.type === "text") schema[name] = Yup.string().nullable();
    if (field.type === "checkbox") schema[name] = Yup.boolean().nullable();
    if (field.type === "number") schema[name] = Yup.number().nullable();
    if (field.type === "date") schema[name] = Yup.date().nullable();
    if (schema[name] === undefined) schema[name] = Yup.string().nullable();

    if (field.required) schema[name] = schema[name].required(getErrorMessage(name));

    if (field.minCharacters) schema[name] = schema[name].min(field?.minCharacters, getErrorMessage('minimumFieldLength',field.minCharacters));
    if (field.maxCharacters) schema[field.name] = schema[field.name].max(field?.maxCharacters,getErrorMessage('maximumFieldLength',field.maxCharacters));

    if (field.type === "checkbox" && field.required) schema[name] = schema[name].oneOf([true],getErrorMessage(name));



    // if we have confirm_, we can have the following case (works for email or password) - the "ref" requires another field's name value
    if (name && name?.indexOf("confirm_") >= 0) {
      if (field.type === "email") {
        schema[name] = schema[name].oneOf(
          [Yup.ref("email"), null],
          getErrorMessage('fieldMatch',name)
        );
      }
      if (field.type === "password") {
        schema[name] = schema[name].oneOf(
          [Yup.ref("password"), null],
          getErrorMessage('fieldMatch',name)
        );
      }
    }
  });
  return Yup.object().shape(schema);
}
Enter fullscreen mode Exit fullscreen mode

In case you have the same structure, you can use the following parsing function to generate initial values in the following structure :

{
 "username" : null,
 "email" : null
}
Enter fullscreen mode Exit fullscreen mode

Generate Initial Values

function getObjectIntialValue(name, value) {
  if (value === false) return { [name]: false };
  return {
    [name]: value || null,
  };
}

export default function generateInitialValues(fields) {
  const initialValues = fields?.map((el) => {
    if (el.type === "text") return getObjectIntialValue(el.name, el.defaultValue);
    if (el.type === "checkbox") return getObjectIntialValue(el.name, el.defaultValue);

    return getObjectIntialValue(el.name);
  });
  const initialValuesParsed = initialValues.reduce((obj, item) => {
    const [key, value] = Object.entries(item)[0];
    obj[key] = value;
    return obj;
  }, {});
  return initialValuesParsed;
}

Enter fullscreen mode Exit fullscreen mode

The use case I needed was to parse the values in order to build a dynamic form using the CMS data to generate initialValues and validationSchema for the FormikWrapper. Moreover, you can add rules to your fields, and match them inside the validationSchema in case you need a proper structure using regex.

Thank you for reading,
Mihai

Top comments (0)