I was recently tasked with creating a wizard form for a coding challenge. I enjoyed the challenge and found that being able to create wizard forms is a useful task for any developer, especially newbies like myself. In this tutorial, I will walk you through how to create a simple two page wizard form that collects a user's first and last name, email, and phone number. So, let's get started and create our form!
Project Setup
TL;DR: create a React project with components: MasterForm, FormStep, and FormReview
Let's begin by creating our react project folder and installing any dependencies. For this project, I'm going to be using Sass for styling, but feel free to write in CSS. (The main focus of this tutorial will be on React logic and not CSS). You can use yarn or npm to install Sass. We won't be needing any other packages or libraries.
npx create-react-app wizard-form
npm install node-sass
Go into your src folder and open App.js. We want a blank slate for our project and styles, so delete all of the content inside of the div with the className App. While you're at it, delete the import logo from './logo.svg
line.
**Note if you are using Sass, don't forget to change the file names in App.js and App.css.
You should be left with:
import './App.scss';
function App() {
return (
<div className="App">
</div>
);
}
export default App;
Now, create a components folder in the src folder and create the following components: MasterForm, FormStep, and FormReview.
MasterForm Component Setup
For this tutorial, I will be using functional components. Go into your MasterForm component and import useState. The most important part of a wizard form are the steps. Our MasterForm component will keep track of the steps and our form will start at the default of step 1. We will also need a prop that keeps track of whether the form is finished or not. We also don't want our users to be able to continue the form without filling out each input. To do this, we also need a prop to keep track of whether the next button is disabled or not.
Make sure your form matches the following:
import { useState } from "react";
const MasterForm = () => {
const [currentStep, setCurrentStep] = useState(1);
const [formComplete, setFormComplete] = useState(false);
const [isDisabled, setIsDisabled] = useState(true);
return (
<>
</>
)
}
export default MasterForm
Now, let's setup the inputs for our form! We will want to collect the following information from our users:
const [inputs, setInputs] = useState({
firstName: "",
lastName: "",
email: "",
phoneNumber: "",
});
Form Validation & Functionality
The biggest challenge when creating a wizard form is adding form validation. To add some basic form validation, add the following to your MasterForm component:
const [isValid, setIsValid] = useState({
firstName: false,
lastName: false,
email: false,
phoneNumber: false,
});
const [error, setError] = useState({
firstName: null,
lastName: null,
email: null,
phoneNumber: null,
});
The error state will change to display an error message based on a switch case we will write next which will also change the isValid state to true if the case is met.
So, let's create our check validity function next. This function will change the state of isValid to true if a condition is passed. Our checkValidity function will take the name and value of the input as arguments. I will be using a switch case for form validation, but feel free to experiment and use a a different method.
const checkValidity = (name, value) => {
switch (name) {
case "firstName":
case "lastName":
case "phoneNumber":
isValid[name] = value !== "";
setError({
...error,
[name]: !isValid[name] ? "Field cannot be blank" : null,
});
break;
case "email":
let pattern =
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
isValid.email = pattern.test(value);
setError({
...error,
email: !isValid.email ? "Enter a valid email" : null,
});
break;
default:
break;
}
};
Great, now we have our validation switch case, but when should it be called? For this small form, I have it being called onChange for each input. We haven't create our handleChange function yet, so let's set that up. Based on the current step of the form, this function will check the validity and set disabled to false if the criteria is met for each input.
const handleChange = (event) => {
setInputs({
...inputs,
[event.target.name]: event.target.value,
});
checkValidity(event.target.name, event.target.value);
if (currentStep === 1) {
if (
!!isValid.firstName &&
!!isValid.lastName
) {
setIsDisabled(false);
} else {
setIsDisabled(true);
}
} else {
if (
!!isValid.email &&
!!isValid.phoneNumber
) {
setIsDisabled(false);
} else {
setIsDisabled(true);
}
}
};
The last things we need to create in our MasterForm component before moving on to creating the actual form steps, are the button functions. Let's create the functionality for the next, cancel, and submit buttons. The cancel button will reset the entire form and the all the fields.
const next = () => {
setIsDisabled(true);
setCurrentStep(currentStep + 1);
};
const cancel = () => {
setInputs({
firstName: "",
lastName: "",
email: "",
phoneNumber: "",
});
setIsValid({
firstName: false,
lastName: false,
email: false,
phoneNumber: false,
});
setCurrentStep(1);
setFormComplete(false);
};
const handleSubmit = (event) => {
event.preventDefault();
setFormComplete(true);
};
Creating The Form Components
For the first step in the form, we want to feed FormStep the following props: handleChange, next, cancel, isDisabled, and inputs. In order to avoid creating two separate FormStep components, we are going to try and make this component as reusable as possible by adding the inputs in the following way:
<form onSubmit={(event) => handleSubmit(event)}>
{currentStep === 1 ? (
<FormStep
handleChange={handleChange}
next={next}
cancel={cancel}
isDisabled={isDisabled}
inputs={[
{
label: "First Name",
type: "text",
name: "firstName",
value: inputs.firstName,
error: error.firstName,
},
{
label: "Last Name",
type: "text",
name: "lastName",
value: inputs.lastName,
error: error.lastName,
},
/>
) : null}
</form>
Great, now we have the props we need to create the our first FormStep component and soon we will actually see something on the page! Open your FormStep file and add in the following:
const FormStep = ({ handleChange, next, cancel, isDisabled, inputs }) => {
return (
<>
{inputs.map((input, index) => (
<div index={`${input.name}-${index}`}>
<p className="error">{input.error}</p>
<label htmlFor={input.name}>{input.label}</label>
<input
type={input.type}
name={input.name}
value={input.value}
placeholder="Required"
onChange={(event) => handleChange(event)}
required
/>
</div>
))}
<div className="button-container">
<button type="button" onClick={cancel} className="cancel">
Cancel
</button>
<button
type="button"
onClick={next}
className="next"
disabled={!!isDisabled}
>
Next
</button>
</div>
</>
);
};
export default FormStep;
Now that we have a reusable component, creating the second step is easy! Just add this underneath the first step, making sure to add in some conditional rendering based on what the current step is.
//Place this below the first step and inside of the </form>. We will have one more step left where we review and submit the form
{currentStep === 2 ? (
<FormStep
handleChange={handleChange}
next={next}
cancel={cancel}
isDisabled={isDisabled}
inputs={[
{
label: "Email",
type: "email",
name: "email",
value: inputs.email,
error: error.email,
},
{
label: "Phone Number",
type: "text",
name: "phoneNumber",
value: inputs.phoneNumber,
error: error.phoneNumber,
},
]}
/>
) : null}
Creating the Review Component
You're doing awesome so far! Just one more component to go. For the FormReview Component, you will want to make sure it has access to the inputs and cancel props and that it is only visible when the currentStep is 3. This can look like the following:
{currentStep === 3 ? (
<FormReview inputs={inputs} cancel={cancel} />
) : null}
Now, let's build out the review component! Since we just want to display the information for users to review, each of these inputs is going to be automatically disabled. We are also going to add a submit button.
const FormReview = ({ inputs, cancel }) => {
return (
<>
<label htmlFor="firstNameReview" className="review">
FirstName
</label>
<input value={inputs.firstName} disabled="true" name="firstNameReview" />
<label htmlFor="lastNameReview" className="review">
Last Name
</label>
<input value={inputs.lastName} disabled="true" name="lastNameReview" />
<label htmlFor="emailReview" className="review">
Email
</label>
<input value={inputs.email} disabled="true" name="emailReview" />
<label htmlFor="phoneNumberReview" className="review">
Phone Number
</label>
<input value={inputs.phoneNumber} disabled="true" name="phoneNumberReview" />
<div className="button-container">
<button type="button" onClick={cancel} className="cancel">
Cancel
</button>
<button type="submit" className="next">
Submit
</button>
</div>
</>
);
};
export default FormReview;
If you look at our handleSubmit function we created earlier, you'll notice that is sets profileComplete to true. Let's take advantage of that and just create a little note that lets users know that their form was successfully submitted. Add this below the .
{!!formComplete ? (
<>
<p>Form has been successfully submitted. Thank you.</p>
<button type="button" onClick={cancel} className="next">
Add Another User
</button>
</>
) : null}
Review
If you've followed the tutorial, you should have successfully created a basic wizard form. You also learned how to add basic form validation and created a reusable formStep component. The opportunities to expand on this our endless. Read on for some inspiration!
Next Steps & Ideas
My challenge to you is to add another component to your form called FormProgress. Play around and figure out how to track the progress of the form based on the currentStep. Another challenge could be using different types of inputs like radio buttons or textareas. Can you figure out how to make that formStep a reusable component still?
Ideas: Wizard forms don't have to be boring! Here are some I have created in the past. I recommend practice with something fun and around a topic that you're interested in.
Check out this one live here.
If you've made it this far, thank you for reading and have fun building! ✨