π¨ 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:
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:
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.
Now that we have our hook ready, we will modify our JSX.
First in the form tag we place the handleSubmit and the noValidate.
<formnoValidateonSubmit={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).
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.
import*asYupfrom'yup';import{useFormik}from"formik";import{Layout}from"../components"exportconstFormikBasic=()=>{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 (<Layouttitle="Formik Basic"><formnoValidateonSubmit={handleSubmit}><inputtype="text"placeholder="Full name"{...getFieldProps('fullName')}className={`${(touched.fullName&&errors.fullName)&&'error_input'}`}/>{(touched.fullName&&errors.fullName)&&<spanclassName="error">{errors.fullName}</span>}<inputtype="email"placeholder="E-mail"{...getFieldProps('email')}className={`${(touched.email&&errors.email)&&'error_input'}`}/>{(touched.email&&errors.email)&&<spanclassName="error">{errors.email}</span>}<inputtype="password"placeholder="Password"{...getFieldProps('password')}className={`${(touched.password&&errors.password)&&'error_input'}`}/>{(touched.password&&errors.password)&&<spanclassName="error">{errors.password}</span>}<div><labelhtmlFor="rol">Select an option:</label><selectid="rol"{...getFieldProps('rol')}><optionvalue="">--- Select ---</option><optionvalue="admin">Admin</option><optionvalue="user">User</option><optionvalue="super">Super Admin</option></select></div><divclassName='radio-group'><b>Gender: </b><label><inputtype="radio"{...getFieldProps('gender')}checked={getFieldProps('gender').value==='man'}value='man'/>
Man
</label><label><inputtype="radio"{...getFieldProps('gender')}checked={getFieldProps('gender').value==='women'}value='women'/>
Woman
</label><label><inputtype="radio"{...getFieldProps('gender')}checked={getFieldProps('gender').value==='other'}value='other'/>
Other
</label>{(touched.gender&&errors.gender)&&<spanclassName="error">{errors.gender}</span>}</div>{(touched.rol&&errors.rol)&&<spanclassName="error">{errors.rol}</span>}<label><inputtype="checkbox"{...getFieldProps('terms')}/>
Terms and Conditions
{(touched.terms&&errors.terms)&&<spanclassName="error">{errors.terms}</span>}</label><buttontype="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:
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.
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.
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').
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.
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.
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.
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.
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.
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.
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.
exportconstforms:{[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.
constgenerateValidations=(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.
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.
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.
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.
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
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.
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