Dynamic forms with Formik and React JS. πŸ“

franklin030601

Franklin Martinez

Posted on October 28, 2022

Dynamic forms with Formik and React JS. πŸ“

This time I will show you how to create dynamic forms with Formik and Yup, using React JS with TypeScript.

Any kind of feedback is welcome, thanks and I hope you enjoy the article.πŸ€—

⚠️ Note: You need to have basic knowledge in React JS and hooks and TypeScript.

Β 

Table of contents

πŸ“Œ Technologies to be used.

πŸ“Œ Creating the project.

πŸ“Œ First steps.

πŸ“Œ Designing the form.

πŸ“Œ Implementing Formik in our form.

πŸ“Œ Designing our dynamic form.

πŸ“Œ Creating the Input component.

πŸ“Œ Creating the Checkbox component.

πŸ“Œ Creating the RadioGroup component.

πŸ“Œ Creating the Select component.

πŸ“Œ Creating the form object.

πŸ“Œ Generating the validation rules using functions.

πŸ“Œ Initializing the form values.

πŸ“Œ Using the getInputs function.

πŸ“Œ Conclusion.

πŸ“Œ Source code.

Β 

πŸ–ŠοΈ Technologies to be used.

  • ▢️ React JS (v 18)
  • ▢️ Vite JS
  • ▢️ TypeScript
  • ▢️ Formik
  • ▢️ CSS vanilla (You can find the styles in the repository at the end of this post)

πŸ–ŠοΈ Creating the project.

We will name the project: formik-dynamic (optional, you can name it whatever you like).



npm init vite@latest


Enter fullscreen mode Exit fullscreen mode

We create the project with Vite JS and select React with TypeScript.

Then we run the following command to navigate to the directory just created.



cd formik-dynamic


Enter fullscreen mode Exit fullscreen mode

Then we install the dependencies.



npm install


Enter fullscreen mode Exit fullscreen mode

Then we open the project in a code editor (in my case VS code).



code .


Enter fullscreen mode Exit fullscreen mode

πŸ–ŠοΈ First steps.

We go to the src/App.tsx file and delete all the content to create a new component. That for the moment only render a hello world.



const App = () => {
  return (
    <>
        <div>Hello world</div>
    </>
  )
}
export default App


Enter fullscreen mode Exit fullscreen mode

Lo que sigue es crear un Layout, esto solo es con propΓ³sito de estΓ©tica para la app, no es obligatorio.

🚨 Note: Each time we create a new folder, we will also create an index.ts file to group and export all the functions and components of other files that are inside the same folder, so that these functions can be imported through a single reference, this is known as barrel file.

Create the src/components folder and inside create the Layout.tsx file to add:



interface ILayout {
    children: JSX.Element | JSX.Element[]
    title: string
}

export const Layout = ({ children, title }: ILayout) => {
    return (
        <div className="container">
            <h2 className="title">{title}</h2>
            {children}
        </div>
    )
}


Enter fullscreen mode Exit fullscreen mode

Then we are going to create the src/pages folder. The purpose is to simulate different pages since we are going to explain a basic way in which formik is usually used, then another page will be to explain the dynamic forms.

We will create a FormikBasic.tsx file to add the following for the moment:



import { Layout } from "../components"

export const FormikBasic = () => {
    return (
        <Layout title="Formik Basic">
            <div>Hello world</div>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

Then we import it in the file src/App.tsx.



import { FormikBasic } from "./pages"

const App = () => {
  return (
    <>
      <FormikBasic />
    </>
  )
}
export default App


Enter fullscreen mode Exit fullscreen mode

πŸ–ŠοΈ Designing the form.

Inside src/components/FormikBasic.tsx we are going to create a basic form, which uses some of the most common controls in a form.



import { Layout } from "../components"

export const FormikBasic = () => {

    return (
        <Layout title="Formik Basic">
            <form>

                <input type="text" placeholder="Full name"/>

                <input type="email" placeholder="E-mail"/>

                <input type="password" placeholder="Password"/>

                <div>
                    <label htmlFor="rol">Select an option:</label>
                    <select id="rol" >
                        <option value="">--- Select ---</option>
                        <option value="admin">Admin</option>
                        <option value="user">User</option>
                        <option value="super">Super Admin</option>
                    </select>
                </div>

                <div className='radio-group'>
                    <b>Gender: </b>
                    <label ><input type="radio"/> Man</label>
                    <label ><input type="radio"/> Woman</label>
                    <label ><input type="radio"/> Other</label>
                </div>

                <label>
                    <input type="checkbox" {...getFieldProps('terms')} />
                    Terms and Conditions
                </label>

                <button type="submit">Submit</button>
            </form>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

Already with styles, it should look like this πŸ‘€:

basic form

πŸ–ŠοΈ Implementing Formik in our form.

We proceed to install Formik to manage our form and another very useful package will be Yup to manage the validations of our form.



npm install formik yup


Enter fullscreen mode Exit fullscreen mode

Normally to use Formik and manage our forms we would do it through the hook useFormik.

To the hook use Formik we will pass an object with 3 properties (it has more properties but we will only use these 3)

  • initialValues, is an object with the initial values of our form, that makes reference to each input or field.
  • validationSchema, this is basically where we will use Yup to establish the validations to our respective form fields.
  • onSubmit, is a function that receives the values of the form and is only executed when all the validations are passed.


useFormik({
    initialValues:,
    validationSchema: ,
    onSubmit:
});


Enter fullscreen mode Exit fullscreen mode

initialValues, will have an object defining each field of the form and will be initialized with an empty string and the terms field will be a boolean value initialized to false.

validationSchema, will have a Yup.object (Don't forget to import Yup at the top of the file: import * as Yup from 'yup' ), which is a function that receives an object defining the validations. (Note that the keys of each property must match the properties of initialValues). Inside each property will have its validation rules

onSubmit, in fact it will do nothing more than show the values in console.



const { handleSubmit, errors, touched, getFieldProps } = useFormik({
    initialValues: {
        fullName: '',
        email: '',
        password: '',
        rol: '',
        gender: '',
        terms: false
    },
    validationSchema: Yup.object({
        fullName: Yup.string().min(3, 'Min. 3 characters').required('Required'),
        email: Yup.string().email('It should be a valid email').required('Required'),
        password: Yup.string().min(6, 'Min. 6 characters').required('Required'),
        terms: Yup.boolean().isTrue('You must accept the terms!'),
        rol: Yup.string().required('Required'),
        gender: Yup.string().required('Required'),
    }),
    onSubmit: values => {
        console.log(values)
    }
});


Enter fullscreen mode Exit fullscreen mode

The hook returns several properties and functions, but we only need:

  • handleSubmit, it is the function that you must pass to your form to make the form post. This function must be executed in the onSubmit event of the form tag.

  • errors, is an object with the errors of each field, identified with the name of the properties that you placed in initialValues.

  • touted, indicates if the input has been touched, this will serve to execute the validations of that field and to show the error after the input has been touched and not at the beginning of the application when the user just sees the form. This prop, is an object whose props are identified with the name of the properties that you placed in initialValues.

  • getFieldProps, is a getter function that brings us different attributes necessary for the input to work (name, value, onChange, onBlur) that generally we can also obtain them of the useFormik, but it would be more code to have to place each property (it is useful when we have to do something specific with this property. but in this case not). It receives as parameter a name, that must with some property of initialValues.



const { handleSubmit, errors, touched, getFieldProps } = useFormik({
    // ...props
});


Enter fullscreen mode Exit fullscreen mode

Now that we have our hook ready, we will modify our JSX.

First in the form tag we place the handleSubmit and the noValidate.



<form noValidate onSubmit={handleSubmit}>


Enter fullscreen mode Exit fullscreen mode

Now in the input of type text, email and password, we place the following.

We spread the properties that getFieldProps returns (it receives as parameter a name, which must have some property of initialValues).

The className we do the validation where if the input was touched and there is the corresponding error with that input, that the class 'error_input' is added. (although I never actually use that class in the styles).



<input
    // ...attr
    {...getFieldProps('password')}
    className={`${(touched.password && errors.password) && 'error_input'}`}
/>


Enter fullscreen mode Exit fullscreen mode

Such a condition in the className attribute can be used to display the error message:



{(touched.password && errors.password) && <span className="error">{errors.password}</span>}


Enter fullscreen mode Exit fullscreen mode

In the case of the select and the input of the checkbox type, we will only add the getFieldProps and spread the values returned by this function.



<select id="rol" {...getFieldProps('rol')} >
    // ...options
</select>

<input type="checkbox" {...getFieldProps('terms')} />


Enter fullscreen mode Exit fullscreen mode

In the case of the radio type input.
We add the getFieldProps and we spread the values returned by this function.

We will add a value.

In the checked property we will evaluate if the value property of getFieldProps is equal to the value of our input, then it must be active that input radio.



<input type="radio"
    {...getFieldProps('gender')}
    value='women'
    checked={getFieldProps('gender').value === 'women'}
/>


Enter fullscreen mode Exit fullscreen mode

Our entire component would look as follows πŸ‘€:



import * as Yup from 'yup';
import { useFormik } from "formik";
import { Layout } from "../components"

export const FormikBasic = () => {

    const { handleSubmit, errors, touched, getFieldProps } = useFormik({
        initialValues: {
            fullName: '',
            email: '',
            password: '',
            rol: '',
            gender: '',
            terms: false
        },
        validationSchema: Yup.object({
            fullName: Yup.string().min(3, 'Min. 3 characters').required('Required'),
            email: Yup.string().email('It should be a valid email').required('Required'),
            password: Yup.string().min(6, 'Min. 6 characters').required('Required'),
            terms: Yup.boolean().isTrue('You must accept the terms!'),
            rol: Yup.string().required('Required'),
            gender: Yup.string().required('Required'),
        }),
        onSubmit: values => {
            // TODO: some action
        }
    });

    return (
        <Layout title="Formik Basic">
            <form noValidate onSubmit={handleSubmit}>

                <input
                    type="text"
                    placeholder="Full name"
                    {...getFieldProps('fullName')}
                    className={`${(touched.fullName && errors.fullName) && 'error_input'}`}
                />
                {(touched.fullName && errors.fullName) && <span className="error">{errors.fullName}</span>}
                <input
                    type="email"
                    placeholder="E-mail"
                    {...getFieldProps('email')}
                    className={`${(touched.email && errors.email) && 'error_input'}`}
                />
                {(touched.email && errors.email) && <span className="error">{errors.email}</span>}
                <input
                    type="password"
                    placeholder="Password"
                    {...getFieldProps('password')}
                    className={`${(touched.password && errors.password) && 'error_input'}`}
                />
                {(touched.password && errors.password) && <span className="error">{errors.password}</span>}
                <div>
                    <label htmlFor="rol">Select an option:</label>

                    <select id="rol" {...getFieldProps('rol')} >
                        <option value="">--- Select ---</option>
                        <option value="admin">Admin</option>
                        <option value="user">User</option>
                        <option value="super">Super Admin</option>
                    </select>
                </div>


                <div className='radio-group'>
                    <b>Gender: </b>
                    <label >
                        <input type="radio"
                            {...getFieldProps('gender')}
                            checked={getFieldProps('gender').value === 'man'}
                            value='man'
                        />
                        Man
                    </label>
                    <label >
                        <input type="radio"
                            {...getFieldProps('gender')}
                            checked={getFieldProps('gender').value === 'women'}
                            value='women'
                        />
                        Woman
                    </label>
                    <label >
                        <input type="radio"
                            {...getFieldProps('gender')}
                            checked={getFieldProps('gender').value === 'other'}
                            value='other'
                        />
                        Other
                    </label>

                    {(touched.gender && errors.gender) && <span className="error">{errors.gender}</span>}
                </div>
                {(touched.rol && errors.rol) && <span className="error">{errors.rol}</span>}
                <label>
                    <input type="checkbox" {...getFieldProps('terms')} />
                    Terms and Conditions
                    {(touched.terms && errors.terms) && <span className="error">{errors.terms}</span>}
                </label>

                <button type="submit">Submit</button>
            </form>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

So far we have a basic and functional use of a form with its validations.

πŸ–ŠοΈ Designing our dynamic form.

Now we are going to create a new page, in our src/pages we create FormikDynamic.tsx.
And for the moment we add:



import { Layout } from "../components"

export const FormikDynamic = () => {
    return (
        <Layout title="Formik Dynamic">
            <div>Hello world</div>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

And this component is shown in src/App.tsx.



import { FormikBasic, FormikDynamic } from "./pages"

const App = () => {
  return (
    <>
      <FormikDynamic />
      {/* <FormikBasic /> */}
    </>
  )
}
export default App


Enter fullscreen mode Exit fullscreen mode

Inside src/components/FormikDynamic.tsx we are going to add the components that Formik offers us to manage a form.

We import the Formik component, whose props are similar to the useFormik hook and that at the same time we will use the same 3 properties mentioned above. Then we will set the initialValues and validationSchema.



import { Formik } from "formik"

export const FormikDynamic = () => {
    return (
        <Layout title="Formik Dynamic">

            <Formik
                initialValues={{}}
                validationSchema={{}}
                onSubmit={ values => console.log(values) }
            > 

            </Formik>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

The Formik component uses the "render props" pattern and for this purpose it receives a function inside the component. This function also returned certain properties as does the hook useFormik, but in this case we will not use them.

The function, is going to render another component of formik that is the Form since it is similar to the form tag but that already has the handleSubmit.



import { Formik } from "formik"

export const FormikDynamic = () => {
    return (
        <Layout title="Formik Dynamic">

            <Formik
                initialValues={{}}
                validationSchema={{}}
                onSubmit={ values => console.log(values) }
            > 
            {
                () => (
                    <Form noValidate>

                        <button className="btn btn_submit" type="submit">Submit</button>
                    </Form>
                )
            }         
            </Formik>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

Now we need the inputs, but in this case we will separate them into reusable components.

✏️ Creating the Input component.

Inside the folder src/components we create the file CustomTextInput.tsx.

In the file we create a component and define the interface of the props that will arrive to the component.

The name is one of the most important parts to identify the input.

At the end we put [x: string]: any because if you need to put some other prop, you do not have to establish it in the interface avoiding that it grows (it is optional, if you want to define each property you can do it).

For example, in the interface we don't have defined the prop autoComplete but when we use the component, it won't error if we put as props autoComplete and its value ('off' / 'on').

We add a simple input without attributes.



interface Props {
    name: string;
    type: string;
    placeholder?: string;
    [x: string]: any
}

export const CustomTextInput = (props: Props) => {
    return ( <input /> )
}


Enter fullscreen mode Exit fullscreen mode

Now, we will use the useField hook provided by formik. This hook is the key to connect the inputs to Formik.

The useField hook takes either an object or a string as an argument, but you must always send it the name of the input. In this case there is no problem sending the whole prop object, or just prop.name.

The hook returns an array with three positions.

  • FieldProps, contains everything necessary for the input to work onChange, value, onBlur, etc.

  • FieldMetaProps, contains values computed on the field that can be used to style or change the field, such as touched and errors.

  • FieldHelperProps, contains helper functions that allow to imperatively change the values of a field. For example setValue.

The one that interests us is the value of the first position of the FieldProps, and we spread both the props that arrive to the component and the value of field that gives us the hook.



import { useField } from "formik"

interface Props {
    name: string;
    type: string;
    placeholder?: string;
    [x: string]: any
}

export const CustomTextInput = (props: Props) => {

    const [field] = useField(props)

    return (
        <input {...field} {...props} />
    )
}


Enter fullscreen mode Exit fullscreen mode

You could also use the second position (the FieldMetaProps) to display errors, which would be exactly the same as in Formik basic. But we better use a component provided by formik to display the errors, the ErrorMessage.

The ErrorMessage receives mandatory the name of the input, by default it does not only show the text, without HTML tag so we place the component property to tell it to render a span.



import { ErrorMessage, useField } from "formik"

interface Props {
    name: string;
    type: string;
    placeholder?: string;
    [x: string]: any
}

export const CustomTextInput = (props: Props) => {

    const [field] = useField(props)

    return (
        <>
            <input {...field} {...props} />
            <ErrorMessage name={props.name} component="span" className="error" />
        </>
    )
}


Enter fullscreen mode Exit fullscreen mode

There we have our input.

✏️ Creating the Checkbox component.

Making this input component of type checkbox is basically the same as the normal input. The only thing that changes is the JSX structure and the interface.



import { ErrorMessage, useField } from "formik"

interface Props {
    label: string;
    name: string;
    [x: string]: any
}


export const CustomCheckBox = (props: Props) => {
    const [field] = useField(props)

    return (
        <label className="label_check">
            <input type="checkbox" {...field} {...props} />
            <span>{props.label}</span>
            <ErrorMessage name={props.name} component="span" className="error" />
        </label>
    )
}


Enter fullscreen mode Exit fullscreen mode

✏️ Creating the RadioGroup component.

This component consists of a group of radio type inputs.

It is almost the same as the previous components, only here we have an array of options that are the values and descriptions of the input.

We have to go through these options and set its value attribute to the input and also its checked attribute that will have a condition where if the value of the field is equal to the value of the input the input will be activated.

Placing the value is necessary because this input will not change its value will always be the same, what changes is the value of the checked attribute.

We also put the value to identify the input, since the input radio, so that you can select only one of a group, they have to have the same attribute name.

So when the input does an onChange, formik grabs the value attribute and sets them. then it evaluates if the set value is equal to one of the values of the input group then its checked attribute will set it to bring.



import { useField, ErrorMessage } from 'formik';

type Opt = { value: string | number, desc: string }

interface Props {
    options: Opt[]
    name: string
    label: string
    [x: string]: any
}

export const CustomRadioGroup = ({ label, options, ...props }: Props) => {
    const [field] = useField(props)

    return (
        <div className='radio-group'>
            <b>{label}</b>
            {
                options.map(opt => (
                    <label key={opt.value}>
                        <input
                            {...field}
                            {...props}
                            type="radio"
                            value={opt.value}
                            checked={opt.value === field.value}
                        />
                        {opt.desc}
                    </label>
                ))
            }
            <ErrorMessage name={props.name} component="span" className="error" />
        </div>
    )
}


Enter fullscreen mode Exit fullscreen mode

✏️ Creating the Select component.

To make this component select is almost the same as the radio group. The only thing that changes is the structure of the JSX, the select tag is the one to which the properties of the field and the ones that arrive to our component are spread.

Inside the select, we go through the options and we establish its value and description.



import { ErrorMessage, useField } from "formik"

interface Props {
    options: Opt[]
    label: string;
    name: string;
    [x: string]: any
}

type Opt = { value: string | number, desc: string }

export const CustomSelect = ({ label,options, ...props }: Props) => {
    const [field] = useField(props)

    return (
        <>
            <div>
                <label htmlFor={props.name || props.id}> {label} </label>

                <select {...field} {...props} >

                    <option value="">--- Select ---</option>

                    {
                        options.map(({ desc, value }) => (
                            <option
                                value={value}
                                key={value}
                            >{desc}</option>
                        ))
                    }

                </select>
            </div>
            <ErrorMessage name={props.name} component="span" className="error" />
        </>
    )
}


Enter fullscreen mode Exit fullscreen mode

πŸ–ŠοΈ Creating the form object.

Here we will define how we want our form, it could be either a JSON file, but in this case I will do it with an object to place the types from the beginning.

We create some interfaces.

First we have the InputProps where we have the basic properties of an input and at the end we have 4 properties that are:
-type, will be used to know which component to render.
-typeValue, will be used to know what type of data to assign to the instance of Yup.
-options, are the input radio or the options of a select.
-validations, the validation rules.

Then we have the Opt interface that we had already used before in the CustomRadioGroup and CustomSelect, you can even create an interface file to reuse them.

Finally we have the Validation interface to set the validation rules with Yup.
-type, is the type of validation we want to implement to the field.
-value, the value (optional) that we will set to the validation.
-message, the custom message to display.



export interface InputProps {
    name: string
    value: string | number | boolean
    placeholder?: string
    label?: string

    type: 'text' | 'radio-group' | 'email' | 'password' | 'select' | 'checkbox'
    typeValue?: 'string' | 'boolean'
    options?: Opt[]
    validations: Validation[]
}

export interface Opt {
    value: string | number
    desc: string
}

export interface Validation {
    type: 'required' | 'isEmail' | 'minLength' | 'isTrue'
    value?: string | number | boolean
    message: string
}


Enter fullscreen mode Exit fullscreen mode

Based on the interfaces we export a constant that contains an object with the forms, which in this case is only going to be a form.

Here we have just created exactly the basic form that we have done previously.




export const forms: { [x: string]: InputProps[] } =
{
    login: [
        {
            type: "text",
            name: "name",
            placeholder: "Full Name",
            value: "",
            validations: [
                {
                    type: "minLength",
                    value: 3,
                    message: "Min. 3 characters",
                },
                {
                    type: "required",
                    message: "Full Name is required"
                },
            ],

        },
        {
            type: "email",
            name: "email",
            placeholder: "E-mail",
            value: "",
            validations: [
                {
                    type: "required",
                    message: "Email is required"
                },
                {
                    type: "email",
                    message: "Email no valid"
                }
            ],

        },
        {
            type: "password",
            name: "password",
            placeholder: "Password",
            value: "",
            validations: [
                {
                    type: "required",
                    message: "Password is required"
                }
            ],

        },
        {
            type: "select",
            name: "rol",
            label: "Select an option: ",
            value: "",
            options: [
                {
                    value: "admin",
                    desc: "Admin",
                },
                {
                    value: "user",
                    desc: "User"
                },
                {
                    value: "super-admin",
                    desc: "Super Admin"
                }
            ],
            validations: [
                {
                    type: "required",
                    message: "Rol is required"
                }
            ]
        },
        {
            type: "radio-group",
            name: "gender",
            label: "Gender: ",
            value: "",
            options: [
                {
                    value: 'man',
                    desc: "Man"
                },
                {

                    value: "woman",
                    desc: "Woman"
                },
                {

                    value: "other",
                    desc: "Other"
                },
            ],
            validations: [
                {
                    type: "required",
                    message: "Gender is required"
                }
            ]
        },
        {
            type: "checkbox",
            name: "terms",
            typeValue: "boolean",
            label: "Terms and Conditions",
            value: false,
            validations: [
                {
                    type: "isTrue",
                    message: "Accept the terms!"
                }
            ]
        },
    ],
}


Enter fullscreen mode Exit fullscreen mode

Now it is time to create a function to build each validation rule and the initial values of the form

πŸ–ŠοΈ Generating the validation rules using functions.

We go to src/utils and inside we create a file called getInputs.ts.
Where we are going to have the first function:

generateValidations, it receives only one parameter:

  • field, the first one is the field with all its props, although we only need the validations and the type of value that is the field.


const generateValidations = (field: InputProps) => {}


Enter fullscreen mode Exit fullscreen mode

Then we need to create an empty schema, which we are going to reassign its value.

But for the moment the schema can be a string or a boolean, since the only boolean value is the checkbox and the others are string, but it will accept other primitive values.



const generateValidations = (field: InputProps) => {
    let schema = Yup[field.typeValue ? field.typeValue : 'string']() // Yup.string() 
}


Enter fullscreen mode Exit fullscreen mode

We will then step through each field validation. The validations are found in field.validations.



import * as Yup from "yup";
import { InputProps } from './forms';

const generateValidations = (field: InputProps) => {

    let schema = Yup[field.typeValue ? field.typeValue : 'string']()

    for (const rule of field.validations) {}
}


Enter fullscreen mode Exit fullscreen mode

Then, we are going to evaluate the type of validation that the field has, and we will do it with a switch.

By default, only the required validation and the message are added to the schema.



import * as Yup from "yup";
import { InputProps } from './forms';

const generateValidations = (field: InputProps) => {

    let schema = Yup[field.typeValue ? field.typeValue : 'string']()

    for (const rule of field.validations) {
        switch (rule.type) {

            default: schema = schema.required(rule.message); break;
        }
    }

}


Enter fullscreen mode Exit fullscreen mode

Finally we add the other validations and return the schema.



import * as Yup from "yup";
import { AnyObject } from "yup/lib/types";
import { InputProps } from './forms';

type YupBoolean = Yup.BooleanSchema<boolean | undefined, AnyObject, boolean | undefined>
type YupString = Yup.StringSchema<string | undefined, AnyObject, string | undefined>

const generateValidations = (field: InputProps) => {

    let schema = Yup[field.typeValue ? field.typeValue : 'string']()

    for (const rule of field.validations) {
        switch (rule.type) {

            case 'isTrue': schema = (schema as YupBoolean).isTrue(rule.message); break;

            case 'isEmail': schema = (schema as YupString).email(rule.message); break;

            case 'minLength': schema = (schema as YupString).min(rule?.value as number, rule.message); break;

            default: schema = schema.required(rule.message); break;
        }
    }

    return schema
}


Enter fullscreen mode Exit fullscreen mode

✏️ Initializing the form values.

Now we need to initialize the values of our form.

For it we are going to create a function that receives a parameter.

You will notice that in the file src/utils/forms.ts the constant forms exports an object, well the idea is that the function that we will create now receives a property that coincides with the keys of the object, for example the key 'login'. So that this way we keep the forms in a single file.



export const forms: { [x: string]: InputProps[] } = {
    login: [
        // ...
    ],
    // register:[
        //... 
    // ],
    // etc...
}


Enter fullscreen mode Exit fullscreen mode

Another option is also to pass the complete form to the function.

But we will do it passing only the key.

So we create the function, inside we initialize two variables:

  • initialValues, the initial values of our form.
  • validationsFields**, the validation rules of each field of our form.

These variables will be reassigned again so we use let and initialize them as an empty object.



type Form = 'login'

export const getInputs = (section: Form) => {

    let initialValues: { [key: string]: any } = {};

    let validationsFields: { [key: string]: any } = {};
};


Enter fullscreen mode Exit fullscreen mode

Then we go through the form, accessing its section



import { forms } from './forms';

type Form = 'login'

export const getInputs = (section: Form) => {

    let initialValues: { [key: string]: any } = {};

    let validationsFields: { [key: string]: any } = {};

    for (const field of forms[section]) {}
};


Enter fullscreen mode Exit fullscreen mode

Inside the loop:
1 - We use the initialValues variable to compute the field name and assign the default value of the field.

2 - We will make a condition where if there is no validation for the field, we just place continue so that it just exits the loop but executes the rest of the code that is after the for of loop.

3 - Then we use the function to generate the validation scheme we created earlier. and assign it to a constant.

4 - At the end of the loop, we use the validationsFields variable to compute the field name and assign the generated schema that is in the schema variable.

5 - Finally, after the cycle we return an object with the validation rules, the initial values of the form and the inputs.



import { forms } from './forms';

type Form = 'login'

export const getInputs = (section: Form) => {

    let initialValues: { [key: string]: any } = {};

    let validationsFields: { [key: string]: any } = {};

    for (const field of forms[section]) {

        initialValues[field.name] = field.value;

        if (!field.validations) continue;

        const schema = generateValidations(field)

        validationsFields[field.name] = schema;
    }

    return {
        validationSchema: Yup.object({ ...validationsFields }),
        initialValues,
        inputs: forms[section],
    };
};


Enter fullscreen mode Exit fullscreen mode

The whole file would look like this (you can separate it into different ones if you want to) πŸ‘€



import * as Yup from "yup";
import { AnyObject } from "yup/lib/types";
import { forms, InputProps } from './forms';

type YupBoolean = Yup.BooleanSchema<boolean | undefined, AnyObject, boolean | undefined>
type YupString = Yup.StringSchema<string | undefined, AnyObject, string | undefined>

const generateValidations = (field: InputProps) => {

    let schema = Yup[field.typeValue ? field.typeValue : 'string']()

    for (const rule of field.validations) {
        switch (rule.type) {
            case 'isTrue': schema = (schema as YupBoolean).isTrue(rule.message); break;
            case 'isEmail': schema = (schema as YupString).email(rule.message); break;
            case 'minLength': schema = (schema as YupString).min(rule?.value as number, rule.message); break;
            default: schema = schema.required(rule.message); break;
        }
    }

    return schema
}

type Form = 'login'

export const getInputs = (section: Form) => {

    let initialValues: { [key: string]: any } = {};

    let validationsFields: { [key: string]: any } = {};

    for (const field of forms[section]) {

        initialValues[field.name] = field.value;

        if (!field.validations) continue;

        const schema = generateValidations(field)

        validationsFields[field.name] = schema;
    }

    return {
        validationSchema: Yup.object({ ...validationsFields }),
        initialValues,
        inputs: forms[section],
    };

};


Enter fullscreen mode Exit fullscreen mode

πŸ–ŠοΈ Using the getInputs function.

We go back to the src/pages/FormikDynamic.tsx file and outside the component we use our getInputs function, sending the section we want as parameter, and obtaining the returned values



const { initialValues, inputs, validationSchema } = getInputs('login')


Enter fullscreen mode Exit fullscreen mode

The initial values and the validation scheme are assigned to the Formik component.



<Formik
    initialValues={initialValues}
    validationSchema={validationSchema}
    onSubmit={(values) => { console.log(values) }}
>
// ...


Enter fullscreen mode Exit fullscreen mode

Now inside the Form component, we are going to iterate with the map function, the inputs that we get from the getInputs.

We are going to evaluate the type of input that we are going to render using a switch. And depending on each type, we render a component and we will pass the necessary props, in this TypeScript will help us.



<Form noValidate>
{
    inputs.map(({ name, type, value, ...props }) => {
        switch (type) {
            case "select":
                return <CustomSelect
                    key={name}
                    label={props.label!}
                    name={name}
                    options={props.options!}
                />

            case "radio-group":
                return <CustomRadioGroup
                    label={props.label!}
                    name={name}
                    options={props.options!}
                    key={name} />

            case "checkbox":
                return <CustomCheckBox
                    label={props.label!}
                    key={name}
                    name={name}
                />

            default:
                return <CustomTextInput
                    key={name}
                    name={name}
                    placeholder={props.placeholder}
                    type={type}
                />
    }
})
}


Enter fullscreen mode Exit fullscreen mode

The component would look like this πŸ‘€:



import { Form, Formik } from "formik"
import { CustomCheckBox, CustomRadioGroup, CustomTextInput, CustomSelect, Layout } from "../components"
import { getInputs } from "../utils"

const { initialValues, inputs, validationSchema } = getInputs('login')

export const FormikDynamic = () => {
    return (
        <Layout title="Formik Dynamic">

            <Formik
                {...{ initialValues, validationSchema }}
                onSubmit={(values) => { console.log(values) }}
            >
                {
                    () => (
                        <Form noValidate>
                            {
                                inputs.map(({ name, type, value, ...props }) => {
                                    switch (type) {
                                        case "select":
                                            return <CustomSelect
                                                key={name}
                                                label={props.label!}
                                                name={name}
                                                options={props.options!}
                                            />

                                        case "radio-group":
                                            return <CustomRadioGroup
                                                label={props.label!}
                                                name={name}
                                                options={props.options!}
                                                key={name} />

                                        case "checkbox":
                                            return <CustomCheckBox
                                                label={props.label!}
                                                key={name}
                                                name={name}
                                            />

                                        default:
                                            return <CustomTextInput
                                                key={name}
                                                name={name}
                                                placeholder={props.placeholder}
                                                type={type}
                                            />
                                    }
                                })
                            }

                            <button className="btn btn_submit" type="submit">Submit</button>
                        </Form>
                    )
                }
            </Formik>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

And that's it, so we have a dynamic form, we just modify the form file to add another form or another field to a form, add another rule, without having to modify the component.

Also, you can split the FormikDynamic component in smaller components if you want.

πŸ–ŠοΈ Conclusion.

Implementing dynamic forms is very helpful if your application has several forms, and thanks to the Formik library and the validations with Yup, it is much easier to work such a form management situation.

Another idea I can give you is, imagine you have an API that gives you a JSON with the fields and validations, it can be much more useful, instead of having a file with all the form information.

I hope you liked this post and that it helped you to understand more about how to make dynamic forms with React and Formik. πŸ€—

If you know any other different or better way to perform this functionality feel free to comment πŸ™Œ.

I invite you to check my portfolio in case you are interested in contacting me for a project!. Franklin Martinez Lucas

πŸ”΅ Don't forget to follow me also on twitter: @Frankomtz361

✏️ Source code.

GitHub logo Franklin361 / dynamic-form

Create dynamics form with React and Formik πŸ“

Dynamic forms with Formik and React JS. πŸ“

This time, we are going to create dynamic forms using React JS and Formik!

demo

Β 

Features βš™οΈ

  1. Show on the form
  2. Create dynamic forms
  3. Field validations

Β 

Technologies πŸ§ͺ

  • ▢️ React JS (v 18)
  • ▢️ Vite JS
  • ▢️ TypeScript
  • ▢️ Formik
  • ▢️ CSS vanilla

Β 

Installation 🧰

  1. Clone the repository (you need to have Git installed).
    git clone https://github.com/Franklin361/dynamic-form
Enter fullscreen mode Exit fullscreen mode
  1. Install dependencies of the project.
    npm install
Enter fullscreen mode Exit fullscreen mode
  1. Run the project.
    npm run dev
Enter fullscreen mode Exit fullscreen mode

Β 

Article links ⛓️

Here's the link to the tutorial in case you'd like to take a look at it! eyes πŸ‘€




πŸ’– πŸ’ͺ πŸ™… 🚩
franklin030601
Franklin Martinez

Posted on October 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related