Skip to content

Instantly share code, notes, and snippets.

@Clarity-89
Last active October 12, 2022 13:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Clarity-89/c80a0ffc62b5146e8af7bd779e880ad6 to your computer and use it in GitHub Desktop.
Save Clarity-89/c80a0ffc62b5146e8af7bd779e880ad6 to your computer and use it in GitHub Desktop.
Building Multistep Forms With React Hook Form
//Steps/Contact.js
export const Contact = forwardRef((props, ref) => {
//..
return (
<Form onSubmit={handleSubmit(saveData)}>
//..
<Button ref={ref}>Next {">"}</Button>
</Form>
);
});
//Forms/Button.js
import { forwardRef, useImperativeHandle, useRef } from "react";
export const Button = forwardRef(
({ children, variant = "primary", ...props }, ref) => {
const buttonRef = useRef();
useImperativeHandle(ref, () => ({
click: () => {
buttonRef.current.click();
},
}));
return (
<button className={`btn btn-${variant}`} {...props} ref={buttonRef}>
{children}
</button>
);
}
);
//Steps/Stepper.js
const location = useLocation();
const [steps, setSteps] = useState([]);
useEffect(() => {
setSteps((steps) => [...steps, location.pathname]);
}, [location]);
//Steps/Stepper.js
import { useEffect, useState } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { useAppState } from "../state";
export const Stepper = ({ onStepChange }) => {
const [state] = useAppState();
const location = useLocation();
const [steps, setSteps] = useState([]);
useEffect(() => {
setSteps((steps) => [...steps, location.pathname]);
}, [location]);
const getLinkClass = ({ isActive }) =>
`nav-link ${isActive ? "active" : undefined}`;
const contactInfoMissing =
!state.firstName || !state.email || !state.password;
const isVisited = (step) =>
steps.includes(step) && location.pathname !== step;
const navLinks = [
{
url: "/",
name: "Contact",
state: {
showWarning: isVisited("/") && contactInfoMissing,
showSuccess: isVisited("/") && !contactInfoMissing,
},
},
{
url: "/education",
name: "Education",
state: {
showSuccess: isVisited("/education"),
},
},
{
url: "/about",
name: "About",
state: {
showSuccess: isVisited("/about"),
},
},
{
url: "/confirm",
name: "Confirm",
state: {},
},
];
return (
<nav className="stepper navbar navbar-expand-lg">
<div className="collapse navbar-collapse">
<ol className="navbar-nav">
{navLinks.map(({ url, name, state }) => {
return (
<li className="step nav-item" key={url}>
<StepState
showWarning={state.showWarning}
showSuccess={state.showSuccess}
/>
<NavLink
end
to={url}
className={getLinkClass}
onClick={onStepChange}
>
{name}
</NavLink>
</li>
);
})}
</ol>
</div>
</nav>
);
};
const StepState = ({ showWarning, showSuccess }) => {
if (showWarning) {
return <span className={"warning-sign"}>!</span>;
} else if (showSuccess) {
return (
<div className="checkmark">
<div className="circle"></div>
<div className="stem"></div>
<div className="tick"></div>
</div>
);
} else {
return null;
}
};
//App.js
export const App = () => {
const buttonRef = useRef();
const onStepChange = () => {
buttonRef.current.click();
};
return (
<AppProvider>
<Router>
<Stepper onStepChange={onStepChange} />
<Routes>
<Route path="/" element={<Contact ref={buttonRef} />} />
<Route path="/education" element={<Education ref={buttonRef} />} />
<Route path="/about" element={<About ref={buttonRef} />} />
<Route path="/confirm" element={<Confirm />} />
</Routes>
</Router>
</AppProvider>
);
};
//Forms/Form.js
import { useNavigate } from "react-router-dom";
export const Form = ({ children, onSubmit, nextStep, ...props }) => {
const navigate = useNavigate();
const onSubmitCustom = (e) => {
e.preventDefault();
onSubmit();
navigate(nextStep);
};
return (
<form className="row" onSubmit={onSubmitCustom} {...props} noValidate>
{children}
</form>
);
};
//Steps/Contact.js
import { forwardRef } from "react";
import { useForm } from "react-hook-form";
import { useAppState } from "../state";
import { Button, Field, Form, Input } from "../Forms";
export const Contact = forwardRef((props, ref) => {
const [state, setState] = useAppState();
const { handleSubmit, register } = useForm({
defaultValues: state,
mode: "onSubmit",
});
const saveData = (data) => {
setState({ ...state, ...data });
};
return (
<Form onSubmit={handleSubmit(saveData)} nextStep={"/education"}>
<fieldset>
<legend>Contact</legend>
<Field label="First name">
<Input {...register("firstName")} id="first-name" />
</Field>
<Field label="Last name">
<Input {...register("lastName")} id="last-name" />
</Field>
<Field label="Email">
<Input {...register("email")} type="email" id="email" />
</Field>
<Field label="Password">
<Input {...register("password")} type="password" id="password" />
</Field>
<Button ref={ref}>Next {">"}</Button>
</fieldset>
</Form>
);
});
//Steps/Confirm.js
import { useForm } from "react-hook-form";
import { useAppState } from "../state";
import { Button, Form, Section, SectionRow } from "../Forms";
export const Confirm = () => {
const [state] = useAppState();
const { handleSubmit } = useForm({ defaultValues: state });
const submitData = (data) => {
console.info(data);
// Submit data to the server
};
const data = [
{
title: "Personal info",
url: "/",
items: [
{ name: "First name", value: state.firstName, required: true },
{ name: "Last name", value: state.lastName },
{ name: "Email", value: state.email, required: true },
{
name: "Password",
value: !!state.password ? "*****" : "",
required: true,
},
],
},
{
title: "Education",
url: "/education",
items: [
{ name: "University", value: state.university },
{ name: "Degree", value: state.degree },
],
},
{
title: "About",
url: "/about",
items: [{ name: "About me", value: state.about }],
},
];
return (
<Form onSubmit={handleSubmit(submitData)}>
<h1 className="mb-4">Confirm</h1>
{data.map(({ title, url, items }) => {
return (
<Section title={title} url={url} key={title}>
{items.map(({ name, value }) => {
return (
<SectionRow key={name}>
<div>{name}</div>
<div>{value}</div>
</SectionRow>
);
})}
</Section>
);
})}
<div className="clo-md-12 d-flex justify-content-start">
<Button>Submit</Button>
</div>
</Form>
);
};
//Steps/Confirm.js
const disableSubmit = data.some((section) =>
section.items.some((item) => item.required && !item.value)
);
//Steps/Confirm.js
<Button disabled={disableSubmit}>Submit</Button>
//Steps/Confirm.js
<Section title={title} url={url} key={title}>
{items.map(({ name, value, required }) => {
const isMissingValue = required && !value;
return (
<SectionRow key={name}>
<div className={isMissingValue ? "text-warning" : ""}>
{name}
</div>
<div>
{isMissingValue ? (
<span className={"warning-sign"}>!</span>
) : (
value
)}
</div>
</SectionRow>
);
})}
</Section>
//Steps/Stepper.js
const StepState = ({ showWarning, showSuccess }) => {
if (showWarning) {
return <span className={"warning-sign"}>!</span>;
} else if (showSuccess) {
return (
<div className="checkmark">
<div className="circle"></div>
<div className="stem"></div>
<div className="tick"></div>
</div>
);
} else {
return null;
}
};
//Steps/Stepper.js
import { useEffect, useState } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { useAppState } from "../state";
export const Stepper = ({ onStepChange }) => {
const [state] = useAppState();
const location = useLocation();
const [steps, setSteps] = useState([]);
useEffect(() => {
setSteps((steps) => [...steps, location.pathname]);
}, [location]);
const getLinkClass = ({ isActive }) =>
`nav-link ${isActive ? "active" : undefined}`;
const contactInfoMissing =
!state.firstName || !state.email || !state.password;
const isVisited = (step) =>
steps.includes(step) && location.pathname !== step;
const navLinks = [
{
url: "/",
name: "Contact",
state: {
showWarning: isVisited("/") && contactInfoMissing,
showSuccess: isVisited("/") && !contactInfoMissing,
},
},
{
url: "/education",
name: "Education",
state: {
showSuccess: isVisited("/education"),
},
},
{
url: "/about",
name: "About",
state: {
showSuccess: isVisited("/about"),
},
},
{
url: "/confirm",
name: "Confirm",
state: {},
},
];
return (
<nav className="stepper navbar navbar-expand-lg">
<div className="collapse navbar-collapse">
<ol className="navbar-nav">
{navLinks.map(({ url, name, state }) => {
return (
<li className="step nav-item" key={url}>
<StepState
showWarning={state.showWarning}
showSuccess={state.showSuccess}
/>
<NavLink
end
to={url}
className={getLinkClass}
onClick={onStepChange}
>
{name}
</NavLink>
</li>
);
})}
</ol>
</div>
</nav>
);
};
const StepState = ({ showWarning, showSuccess }) => {
if (showWarning) {
return <span className={"warning-sign"}>!</span>;
} else if (showSuccess) {
return (
<div className="checkmark">
<div className="circle"></div>
<div className="stem"></div>
<div className="tick"></div>
</div>
);
} else {
return null;
}
};
//Steps/Stepper.js
const location = useLocation();
const [steps, setSteps] = useState([]);
useEffect(() => {
setSteps((steps) => [...steps, location.pathname]);
}, [location]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment