Advanced concepts
Jump to steps
Learn how to jump to specific steps by updating the state.
Initial steps
We can go to specific steps by updating the state of the multi-step form. To learn how to do it, clone the following Github repository so that you don't need to start from scratch.
git clone https://github.com/martiserra99/formity-react-docs
Make sure you run the following command to install all the dependencies:
npm install
Steps component
When navigating to the next or previous step, the state of the multi-step form is updated automatically. However, if we want to jump to specific steps, we need to update the state ourselves. For that, we will create a Steps
component with the following code:
// components/steps.tsx
import type { ZodType } from "zod";
import type { State } from "@formity/react";
import { cn } from "@/utils";
import { useMultiStep } from "@/multi-step";
interface StepsProps<T extends object> {
steps: { state: State; check: ZodType }[];
fields: T;
selected: number;
}
export default function Steps<T extends object>({
steps,
fields,
selected,
}: StepsProps<T>) {
return (
<div className="pointer-events-none absolute inset-x-4 top-5 z-50 flex items-center justify-end gap-3">
{steps.map((step, index) => (
<Step
key={index}
fields={fields}
number={index + 1}
state={step.state}
check={step.check}
selected={selected === index}
/>
))}
</div>
);
}
interface StepProps<T extends object> {
fields: T;
number: number;
state: State;
check: ZodType;
selected: boolean;
}
function Step<T extends object>({
fields,
number,
state,
check,
selected,
}: StepProps<T>) {
const { setState } = useMultiStep();
return (
<button
type="button"
onClick={() => setState(state)}
className={cn(
"pointer-events-auto block rounded-full border border-neutral-800 bg-neutral-950 px-4 py-2 text-sm text-white ring-offset-2 ring-offset-black",
"hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-white/10",
{ "bg-neutral-800 ring-2 ring-neutral-800": selected },
{ "bg-neutral-800": check.safeParse(fields).success },
)}
>
{number}
</button>
);
}
The component that we have created renders a button for each step. When a button is clicked, the state is updated accordingly to jump to that specific step.
The props that the component receives are the following:
steps
: Contains information about each step, where each step is represented as an object with the following properties:state
: Defines the multi-step form's state at the time the button is clicked.check
: Specifies the validation rules that must be met for the step to be considered complete.
fields
: Contains all the values of the multi-step form.selected
: Defines what is the selected step.
We will also need to export this component from the components/index.ts
file:
// components/index.ts
export { default as Steps } from "@/components/steps";
// ...
Multi-step form values
To be able to jump to different steps we will manage the form values ourselves. Therefore, we'll add an onChange
callback to the Step
component, which will be triggered every time a form value is updated:
// components/step.tsx
import type { ReactNode } from "react";
import type { UseFormProps } from "react-hook-form";
import { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useMultiStep } from "@/multi-step";
interface StepProps {
defaultValues: UseFormProps["defaultValues"];
resolver: UseFormProps["resolver"];
children: ReactNode;
onChange: (fields: object) => void;
}
export default function Step({
defaultValues,
resolver,
children,
onChange,
}: StepProps) {
const form = useForm({ defaultValues, resolver });
const { onNext } = useMultiStep();
useEffect(() => {
const { unsubscribe } = form.watch((values) => onChange(values));
return () => unsubscribe();
}, [form, onChange]);
return (
<form onSubmit={form.handleSubmit(onNext)} className="relative h-full">
<FormProvider {...form}>{children}</FormProvider>
</form>
);
}
Create schema
Now we are ready to create the schema. Therefore, we can create a schema.tsx
file with the following code:
// schema.tsx
import type { Schema, Form, State } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z, type ZodType } from "zod";
import {
Steps,
Step,
Layout,
TextField,
NumberField,
Listbox,
NextButton,
BackButton,
} from "./components";
import { MultiStep } from "./multi-step";
export type Fields = {
name: string;
surname: string;
age: number;
gender: string;
country: string;
};
export type Values = [Form<object>, Form<object>, Form<object>, Form<object>];
export type Params = {
fields: Fields;
onChange: (fields: Partial<Fields>) => void;
onSubmit: () => void;
};
const steps: { state: State; check: ZodType }[] = [
{
state: {
points: [{ path: [{ type: "list", slot: 0 }], values: {} }],
inputs: { type: "list", list: [] },
},
check: z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
}),
},
{
state: {
points: [
{ path: [{ type: "list", slot: 0 }], values: {} },
{ path: [{ type: "list", slot: 1 }], values: {} },
],
inputs: { type: "list", list: [] },
},
check: z.object({
age: z.number().min(18, "Min. 18").max(100, "Max. 100"),
}),
},
{
state: {
points: [
{ path: [{ type: "list", slot: 0 }], values: {} },
{ path: [{ type: "list", slot: 1 }], values: {} },
{ path: [{ type: "list", slot: 2 }], values: {} },
],
inputs: { type: "list", list: [] },
},
check: z.object({
gender: z.string().nonempty("Required"),
}),
},
{
state: {
points: [
{ path: [{ type: "list", slot: 0 }], values: {} },
{ path: [{ type: "list", slot: 1 }], values: {} },
{ path: [{ type: "list", slot: 2 }], values: {} },
{ path: [{ type: "list", slot: 3 }], values: {} },
],
inputs: { type: "list", list: [] },
},
check: z.object({
country: z.string().nonempty("Required"),
}),
},
];
function isComplete(fields: Fields) {
return steps.every((step) => step.check.safeParse(fields).success);
}
export const schema: Schema<Values, object, Params> = [
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack, getState, setState }) => (
<MultiStep
step="name"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
defaultValues={{
name: params.fields.name,
surname: params.fields.surname,
}}
resolver={zodResolver(steps[0].check)}
onChange={params.onChange}
>
<Steps fields={params.fields} selected={0} steps={steps} />
<Layout
heading="What is your name?"
description="We would like to know what is your name"
fields={[
<TextField key="name" name="name" label="Name" />,
<TextField key="surname" name="surname" label="Surname" />,
]}
button={<NextButton>Next</NextButton>}
/>
</Step>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack, getState, setState }) => (
<MultiStep
step="age"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
defaultValues={{
age: params.fields.age,
}}
resolver={zodResolver(steps[1].check)}
onChange={params.onChange}
>
<Steps fields={params.fields} selected={1} steps={steps} />
<Layout
heading="What is your age?"
description="We would like to know what is your age"
fields={[<NumberField key="age" name="age" label="Age" />]}
button={<NextButton>Next</NextButton>}
back={<BackButton />}
/>
</Step>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack, getState, setState }) => (
<MultiStep
step="gender"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
defaultValues={{
gender: params.fields.gender,
}}
resolver={zodResolver(steps[2].check)}
onChange={params.onChange}
>
<Steps fields={params.fields} selected={2} steps={steps} />
<Layout
heading="What is your gender?"
description="We would like to know what is your gender"
fields={[
<Listbox
key="gender"
name="gender"
label="Gender"
options={[
{ value: "", label: "Select a gender" },
{ value: "man", label: "Man" },
{ value: "woman", label: "Woman" },
{ value: "other", label: "Other" },
]}
/>,
]}
button={<NextButton>Next</NextButton>}
back={<BackButton />}
/>
</Step>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack, getState, setState }) => (
<MultiStep
step="country"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
defaultValues={{
country: params.fields.country,
}}
resolver={zodResolver(steps[3].check)}
onChange={params.onChange}
>
<Steps fields={params.fields} selected={3} steps={steps} />
<Layout
heading="What is your country?"
description="We would like to know what is your country"
fields={[
<TextField key="country" name="country" label="Country" />,
]}
button={
<NextButton
onClick={params.onSubmit}
disabled={!isComplete(params.fields)}
>
Finish
</NextButton>
}
back={<BackButton />}
/>
</Step>
</MultiStep>
),
},
},
];
We have created a Fields
type that represents the multi-step form's values, which are passed via the params
prop of the Formity
component.
Additionally, we have also used the Steps
component in each step, and we have passed the created steps
array to this component.
Finally, we have created the isComplete
function to verify that all steps of the multi-step form are complete, disabling the final step's button when the steps are not complete.
Create form
Now that we have the schema, we can update the App.tsx
file with the following code:
// App.tsx
import { useCallback, useState } from "react";
import { Formity } from "@formity/react";
import { Data } from "./components";
import { schema, Fields, Values, Params } from "./schema";
const initialFields: Fields = {
name: "",
surname: "",
age: 0,
gender: "",
country: "",
};
export default function App() {
const [submitted, setSubmitted] = useState(false);
const [fields, setFields] = useState(initialFields);
const onChange = useCallback(
(values: Partial<Fields>) => {
setFields({ ...fields, ...values });
},
[fields],
);
const onSubmit = useCallback(() => {
setSubmitted(true);
}, []);
if (submitted) {
return (
<Data
data={fields}
onStart={() => {
setSubmitted(false);
setFields(initialFields);
}}
/>
);
}
return (
<Formity<Values, object, Params>
schema={schema}
params={{ fields, onChange, onSubmit }}
/>
);
}