Dynamically create and validate form inputs using React Hooks
Often we see a contact-form site that ensures the user input is clean, correct and useful and prevents the user from proceeding further. If you wondered, how you can create a dynamic input form and implement that feature using React JS. If you did, then this blog is for you.
Most often, the purpose of data validation is to ensure correct user input. But, one must always have form validation on server-side (Backend) code. Having validation on the client-side will only provide a good user experience therefore you might want to consider using it.
At the end of the blog, you will able to create a dynamic order form with validation. here is a small preview of it :

Step #0 Setting up a React Project We will create a simple React application using create-react-app or you can use any online editor like Stackblitz, Codesandbox etc. After creating react app, also install bootstrap, reactstrap package using command
npm install bootstrap reactstrap
import React from 'react'
import OrderDetails from './pages/OrderDetails';
import 'bootstrap/dist/css/bootstrap.min.css'
const App=()=>{
return <OrderDetails/>
}
export default App;
STEP #1 Creating Structure of the project ποΈ
We will set configuration for form-element and writing validation-check,submit form-input logic in OrderDetails component & creating dynamic form-element in Input.js component
Step #2 Setting Configuration for form-element βοΈ
import React , { useState}from 'react'
const OrderDetails = () =>{
const [orderForm , setFormDetails] = useState ({
Email :{
elementType:"input",
elementConfif : {
type:"text",
placeholder:"eg: johndoe@gmail.com"
},
value:"",
validation:{
required:true
},
validationMessage:null,
isValid:false
},
})
const checkValidation=(value,validation)=>{
// validate value of element
}
const inputChangeHandler=(event,element_name)=>{
// change/updated the value of element
}
const orderHandler = (event) =>{
// submit form-input
}
return (
<div className="container p-1 d-flex flex-column">
<div className="form-header text-center"><h2>Enter Order Details</h2></div>
<div className="form-elements p-2">
<form onSubmit={orderHandler}>
{/* form elements */}
</form>
</div>
</div>
)
}
The above code shows the structure of the OrderDetails component. We will create lists of elements that we will be using in this form like Email, Password, Contact_No., Zip_Code, Address. And each of this form element will have a separate object which holds the type, configuration, validation check and default value of that element
| Properties | What it holds? |
| elementType | input/select/textarea |
| elementConfig | type (text,number,email,password),placeholder |
| value | default value of element |
| validation | validation checks : minLength,maxLength,required,sanitizeEmail,sanitizeZipCode |
| validationMessage | message if input is incorrect |
| isValid | boolean value which shows the validity of element |
| touched | boolean value which checks ,Is user has tried to input text in element ? |
Step #3 Creating form-element dynamically
import React , { useState}from 'react'
import Input from '../Component/Input'
const OrderDetails = () =>{
const [orderForm , setFormDetails] = useState ({
email :{
elementType:"input",
elementConfig : {
type:"text",
placeholder:"Email"
},
value:"",
validation:{
required:true
},
validationMessage:null,
isValid:false
},
})
const [formValid,setFormValid]=useState(false);
const checkValidation=(value,validation)=>{
// validate value of element
}
const inputChangeHandler=(event,element_name)=>{
// change/updated the value of element
}
const orderHandler = (event) =>{
// submit form-input
}
let formArray = [];
Object.entries(orderForm).map(elem=>{
inputArray.push({
name:elem[0],
config: elem[1]
})
})
return (
<div className="container p-1 d-flex flex-column">
<div className="form-header text-center"><h2>Enter Order Details</h2></div>
<div className="form-elements p-2">
<form onSubmit={orderHandler}>
{
formArray.map(elem=>
<Input key={elem.name}
label={elem.name}
elementType={elem.config.elementType}
elementConfig={elem.config.elementConfig}
value={elem.config.value}
onChange={(e)=>
inputChangeHandler(e,elem.name)}
isValid={elem.config.isValid}
touched={elem.config.touched}
message={elem.config.validationMessage}
/>
)
}
</form>
</div>
</div>
)
}
The above code will create an array(formArray) that will hold a separate object(name, config) of each element. By iterating through that array, it renders component for each element by passing configuration as a prop.
import React ,{Fragment} from 'react'
const Input = (props) =>{
let element=null
switch (props.elementType) {
case 'input':
element=(
<>
<label id={props.label} className="label text-lead h6">{props.label} </label>
<input type={props.elementConfig.type}
className="form-control m-1 form-control-md"
value={props.value}
onChange={props.changed}
placeholder={props.elementConfig.placeholder}
/>
</>)
break;
default:
break;
}
return(
<Fragment>
{element}
</Fragment>
)
}
Based on the configuration provided as props by OrderDetails, It will render a form element.

STEP #4 Completing form Just like Email, we have to set configuration for Phone_No, Zip_Code, Street, Delivery_mode, Payment_mode with different validation checks and dynamically create an element by passing config as props Make sure, you set the separate case for each element-type i.e, input, textarea, select in Input Component
const [orderForm , setFormDetails] = useState ({
Email :{
elementType:"input",
elementConfig : {
type:"text",
placeholder:"eg: johndoe@gmail.com"
},
value:"",
validation:{
required:true,
sanitizeEmail:true
},
validationMessage:null,
isValid:false
},
Phone_No:{
elementType:"input",
elementConfig:{
type:"number",
placeholder:"eg: +91XXXXXXXXXX",
},
value:"",
validation:{
minLength:10,
maxLength:12,
required:true
},
validationMessage:"Please input valid phone number",
isValid:false
},
Street:{
elementType:"textarea",
elementConfig:{
cols:20,
rows:5,
placeholder:"street_name"
},
value:"",
validation:{
minLength:5,
required:true
},
validationMessage:"Enter your valid street name",
isValid:false,
},
Zip_Code:{
elementType:"input",
elementConfig:{
type:"number",
placeholder:"zipcode"
},
value:"",
validation:{
required:true,
sanitizeZipCode:true
},
validationMessage:"Enter valid zip-code",
isValid:false
},
Payment_mode:{
elementType:"select",
elementConfig:{
options:[{value:"cod",displayValue:"COD"},{value:"debit/credit",displayValue:"Debit/Credit"}]
},
validation:{
required:true
},
validationMessage:"Please select the payment_mode!",
isValid:true
},
Delivery_mode:{
elementType:"select",
elementConfig:{
options:[{value:"default",displayValue:"Default"},{value:"fast",displayValue:"Fast"}]
},
validation:{
required:true
},
validationMessage:"Please select the delivery mode!",
isValid:true
},
})
const [formValid,setFormValid]=useState(false)
const Input = (props) =>{
let element=null
switch (props.elementType) {
case 'input':
element=(
<>
<label id={props.label} className="label text-lead h6">{props.label}</label>
<input type={props.elementConfig.type}
className="form-control m-1 form-control-md"
value={props.value}
onChange={props.changed}
placeholder={props.elementConfig.placeholder}
name={props.key}
/>
</>)
break;
case "textarea":
element=(
<>
<label id={props.label} className="label text-lead h6">{props.label}</label>
<textarea className="form-control m-1 form-control-md"
placeholder={props.elementConfig.placeholder}
value={props.value}
cols={props.elementConfig.cols}
rows={props.elementConfig.rows}
onChange={props.changed} />
</>
)
break;
case 'select':
element=(
<>
<label id={props.label} className="label text-lead h6">{props.label}</label>
<select className="form-control">
<option selected>Open this select menu</option>
{
props.elementConfig.options.map(option=>
<option key={option.value} value={option.value}>{option.displayValue}</option>
)
}
</select>
</>
)
break;
default:
break;
}
return(
<Fragment>
{element}
</Fragment>
)
}
Pretty Simple!β¨
Yes, I know we got an issue here, In case you figured it out,
we are not able to change the value of the form element. π€
As we have written nothing inside inputChangeHandler() and element is filled with its default value. So our next step will be to allow the user to fill the form.
Step #6 Change the value of an element
const inputChangeHandler=(event,element_name)=>{
const elementsList={...orderForm}
const updatingElement={...elementsList[element_name]}
updatingElement.value=event.target.value;
updatingElement.touched=true
updatingElement.isValid=checkValidation(event.target.value,updatingElement.validation)
elementsList[element_name]=updatingElement;
setFormDetails(elementsList);
let isFormValid=true
for (const key in elementsList) {
isFormValid=elementsList[key].isValid && isFormValid
}
setFormValid(isFormValid)
}
inputChangeHandler() takes event and element_name as a input .
With every change in value, check the validation of that element using checkValidation(), and update the element's isValid property based on boolean value return by checkValidation().
This isValid property shows the validity of an element.

Step #7 Setting Validation
const checkValidation=(value,validation)=>{
let isValid = true;
if(validation.required){
isValid = value.trim()!=='' && isValid
}
if(validation.minLength){
isValid=value.length>=validation.minLength && isValid
}
if(validation.maxLength){
isValid=value.length<=validation.maxLength && isValid
}
if(validation.sanitizeEmail){
if(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(value)){
isValid=true
}else{
isValid=false
}
}
if(validation.sanitizeZipCode){
if(/(^\d{6}$)/.test(value)){
isValid=true;
}else{
isValid=false
}
}
console.log(isValid)
return isValid
}
checkValidation() checks the value against the checks provided in the configuration and returns true if all the validation checks passed else false, we will be using regex to validate Email and Zip-Code.
To provide a good user experience by informing the user about incorrect data, change the colour and show the short error message. Use the following code,& paste it inside Input component.
let elementStyle=["form-control m-1 form-control-md"];
if(props.isValid===false && props.touched)
elementStyle.push("bg-danger")
if(props.isValid && props.touched)
elementStyle.push("bg-success")
element=(
<>
<label id={props.label} className="label text-lead h6 ">{props.label}</label>
<input type={props.elementConfig.type}
className={elementStyle.join(' ')}
value={props.value}
onChange={props.changed}
placeholder={props.elementConfig.placeholder}
/>
{(props.isValid===false && props.touched)?<div className="text-muted text-center">{props.message}</div>:""}
</>)
Yeah! We are almost done. π
Step #8 Submitting Form
So far, we are being able to set element configuration, load element dynamically, change the value, validate user-input and show error message based on that result. But, we have to make sure that the user can only submit the form details onto the database when all its inputs are validated.
<div className="text-center mt-2">
<button type="submit" className="m-auto btn btn-success btn-lg" disabled={!formValid}>Submit</button>
</div>
const orderHandler = (event) =>{
event.preventDefault();
let order={}
for (const elem in orderForm) {
order[elem]=orderForm[elem].value
}
//TODO: Post your order-details on to your backend
localStorage.setItem("order-detail",JSON.stringify(order))
}
Looks awesome !!π
Get source codeπ Github
