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
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
Then we install the dependencies.
npm install
Then we open the project in a code editor (in my case VS code).
code .
ποΈ 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
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>
)
}
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>
)
}
Then we import it in the file src/App.tsx
.
import { FormikBasic } from "./pages"
const App = () => {
return (
<>
<FormikBasic />
</>
)
}
export default App
ποΈ 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>
)
}
Already with styles, it should look like this π:
ποΈ 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
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:
});
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)
}
});
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
});
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}>
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'}`}
/>
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>}
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')} />
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'}
/>
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>
)
}
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>
)
}
And this component is shown in src/App.tsx
.
import { FormikBasic, FormikDynamic } from "./pages"
const App = () => {
return (
<>
<FormikDynamic />
{/* <FormikBasic /> */}
</>
)
}
export default App
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>
)
}
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>
)
}
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 /> )
}
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} />
)
}
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" />
</>
)
}
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>
)
}
βοΈ 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>
)
}
βοΈ 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" />
</>
)
}
ποΈ 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
}
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!"
}
]
},
],
}
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) => {}
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()
}
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) {}
}
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;
}
}
}
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
}
βοΈ 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...
}
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 } = {};
};
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]) {}
};
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],
};
};
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],
};
};
ποΈ 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')
The initial values and the validation scheme are assigned to the Formik component.
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values) => { console.log(values) }}
>
// ...
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}
/>
}
})
}
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>
)
}
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.
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!
Β
Features βοΈ
- Show on the form
- Create dynamic forms
- Field validations
Β
Technologies π§ͺ
-
βΆοΈ React JS (v 18) -
βΆοΈ Vite JS -
βΆοΈ TypeScript -
βΆοΈ Formik -
βΆοΈ CSS vanilla
Β
Installation π§°
- Clone the repository (you need to have Git installed).
git clone https://github.com/Franklin361/dynamic-form
- Install dependencies of the project.
npm install
- Run the project.
npm run dev
Β
Top comments (6)
Hey Franklin,
Could you please tell me the advantage of passing the html in the layout component as props as u could have done it simply
And from here you passed it as children
By the way ,,,
Thanks π for the amazing content βΊοΈ
In Typescript I have to specify the type of Prop that the component receives.
I also use a Layout to
And thanks to you for reading it π
great, just what I needed, thanks
ππ¨βπ» Great article, Franklin! Dynamic forms with Formik and React JS is a powerful combination for creating complex and dynamic forms with ease. Your clear explanations and code samples make it easy for developers to follow along and implement this technology in their own projects. Excellent work!!π
ππAlso, I came across a helpful resource on setting up dynamic forms in Salesforce using FormAssembly. It's called FormAssembly - Setup dynamic Forms in Salesforce and it provides a comprehensive guide on how to use the FormAssembly tool to create dynamic forms.
I really liked this article. It's simple and well-crafted. I'm racking my brain to find the best way to update the form with a useEffect ... Is it possible to change iniitalValue and input arrays? Thank you very much
This is a great writeup and absolutely helpful. π