Advanced concepts
Jump to steps
Learn how to jump to specific steps by updating the state.
Multi-step form
We can jump to specific steps by updating the state of the multi-step form. To add this functionality, we will create the multi-step form in a different way, and we'll start by updating the schema.tsx
file with the following code.
// schema.tsx
import type { Schema, Form } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormStep,
FormStepContent,
FormStepHeading,
FormStepInputs,
FormStepRow,
} from "./components/form-step";
import { Select } from "./components/input/select";
import { TextInput } from "./components/input/text-input";
import { NumberInput } from "./components/input/number-input";
import { NextButton } from "./components/buttons/next-button";
import { BackButton } from "./components/buttons/back-button";
import { MultiStep } from "./multi-step";
export type Values = [Form<object>, Form<object>, Form<object>, Form<object>];
export const schema: Schema<Values> = [
{
form: {
values: () => ({}),
render: ({ onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="nameSurname"
defaultValues={{ name: "", surname: "" }}
resolver={zodResolver(
z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
)}
>
<FormStepContent>
<FormStepHeading>What is your name?</FormStepHeading>
<FormStepInputs>
<FormStepRow>
<TextInput name="name" label="Name" placeholder="Your name" />
<TextInput
name="surname"
label="Surname"
placeholder="Your surname"
/>
</FormStepRow>
</FormStepInputs>
<NextButton>Next</NextButton>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="age"
defaultValues={{ age: 0 }}
resolver={zodResolver(
z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
)}
>
<FormStepContent>
<FormStepHeading>What is your age?</FormStepHeading>
<FormStepInputs>
<NumberInput name="age" label="Age" placeholder="Your age" />
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="gender"
defaultValues={{ gender: "" }}
resolver={zodResolver(
z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
)}
>
<FormStepContent>
<FormStepHeading>What is your gender?</FormStepHeading>
<FormStepInputs>
<Select
name="gender"
label="Gender"
options={[
{ value: "", label: "Select your gender" },
{ value: "man", label: "Man" },
{ value: "woman", label: "Woman" },
{ value: "other", label: "Other" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="country"
defaultValues={{ country: "" }}
resolver={zodResolver(
z.object({
country: z.string().nonempty({ message: "Required" }),
}),
)}
>
<FormStepContent>
<FormStepHeading>What is your country?</FormStepHeading>
<FormStepInputs>
<Select
name="country"
label="Country"
options={[
{ value: "", label: "Select your country" },
{ value: "spain", label: "Spain" },
{ value: "france", label: "France" },
{ value: "germany", label: "Germany" },
{ value: "italy", label: "Italy" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
];
As you may have noticed, the form values are not provided using the values
function. That's because we'll handle the form values in a different way.
Form fields
We'll create a fields
object with all the values of all the forms of the multi-step form. This object will be passed using the params
prop of the Formity
component.
To do it, we first need to update the schema.tsx
file as shown below.
// schema.tsx
import type { Schema, Form } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormStep,
FormStepContent,
FormStepHeading,
FormStepInputs,
FormStepRow,
} from "./components/form-step";
import { Select } from "./components/input/select";
import { TextInput } from "./components/input/text-input";
import { NumberInput } from "./components/input/number-input";
import { NextButton } from "./components/buttons/next-button";
import { BackButton } from "./components/buttons/back-button";
import { MultiStep } from "./multi-step";
export type Values = [Form<object>, Form<object>, Form<object>, Form<object>];
export type Params = {
fields: Fields;
};
export type Fields = {
name: string;
surname: string;
age: number;
gender: string;
country: string;
};
export const schema: Schema<Values, object, Params> = [
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="nameSurname"
defaultValues={{
name: params.fields.name,
surname: params.fields.surname,
}}
resolver={zodResolver(
z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
)}
>
<FormStepContent>
<FormStepHeading>What is your name?</FormStepHeading>
<FormStepInputs>
<FormStepRow>
<TextInput name="name" label="Name" placeholder="Your name" />
<TextInput
name="surname"
label="Surname"
placeholder="Your surname"
/>
</FormStepRow>
</FormStepInputs>
<NextButton>Next</NextButton>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="age"
defaultValues={{ age: params.fields.age }}
resolver={zodResolver(
z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
)}
>
<FormStepContent>
<FormStepHeading>What is your age?</FormStepHeading>
<FormStepInputs>
<NumberInput name="age" label="Age" placeholder="Your age" />
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="gender"
defaultValues={{ gender: params.fields.gender }}
resolver={zodResolver(
z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
)}
>
<FormStepContent>
<FormStepHeading>What is your gender?</FormStepHeading>
<FormStepInputs>
<Select
name="gender"
label="Gender"
options={[
{ value: "", label: "Select your gender" },
{ value: "man", label: "Man" },
{ value: "woman", label: "Woman" },
{ value: "other", label: "Other" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="country"
defaultValues={{ country: params.fields.country }}
resolver={zodResolver(
z.object({
country: z.string().nonempty({ message: "Required" }),
}),
)}
>
<FormStepContent>
<FormStepHeading>What is your country?</FormStepHeading>
<FormStepInputs>
<Select
name="country"
label="Country"
options={[
{ value: "", label: "Select your country" },
{ value: "spain", label: "Spain" },
{ value: "france", label: "France" },
{ value: "germany", label: "Germany" },
{ value: "italy", label: "Italy" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
];
Then, we can update the app.tsx
file as you can see here.
// app.tsx
import { useState } from "react";
import { Formity } from "@formity/react";
import { schema, type Values, type Params, type Fields } from "./schema";
const initialFields: Fields = {
name: "",
surname: "",
age: 0,
gender: "",
country: "",
};
export default function App() {
const [fields] = useState(initialFields);
return (
<Formity<Values, object, Params> schema={schema} params={{ fields }} />
);
}
Field changes
We need to update the fields
object every time form values are changed. To do it, we need to include an onChange
prop to the FormStep
component as shown below.
// components/form-step.tsx
import type { ReactNode } from "react";
import type { DefaultValues, Resolver } from "react-hook-form";
import type { Fields } from "@/schema";
import { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useMultiStep } from "@/multi-step";
interface FormStepProps<T extends Record<string, unknown>> {
defaultValues: DefaultValues<T>;
resolver: Resolver<T>;
children: ReactNode;
onChange: (fields: Partial<Fields>) => void;
}
export function FormStep<T extends Record<string, unknown>>({
defaultValues,
resolver,
children,
onChange,
}: FormStepProps<T>) {
const form = useForm({ defaultValues, resolver });
const { onNext } = useMultiStep();
useEffect(() => {
const { unsubscribe } = form.watch((values) => {
const fields = values as Partial<Fields>;
onChange(fields);
});
return () => unsubscribe();
}, [form, onChange]);
return (
<form
onSubmit={form.handleSubmit(onNext)}
className="flex h-screen w-full items-center justify-center px-4 py-8 font-sans"
>
<FormProvider {...form}>{children}</FormProvider>
</form>
);
}
interface FormStepContentProps {
children: ReactNode;
}
export function FormStepContent({ children }: FormStepContentProps) {
return <div className="w-full max-w-md">{children}</div>;
}
interface FormStepHeadingProps {
children: ReactNode;
}
export function FormStepHeading({ children }: FormStepHeadingProps) {
return (
<h2 className="mb-6 text-center text-4xl font-semibold text-white">
{children}
</h2>
);
}
interface FormStepInputsProps {
children: ReactNode;
}
export function FormStepInputs({ children }: FormStepInputsProps) {
return <div className="mb-6 flex flex-col gap-4">{children}</div>;
}
interface FormStepRowProps {
children: ReactNode;
}
export function FormStepRow({ children }: FormStepRowProps) {
return <div className="flex gap-4">{children}</div>;
}
The function will be passed using the params
prop of the Formity
component. For this reason, we need to update the schema.tsx
file as shown below.
// schema.tsx
import type { Schema, Form } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormStep,
FormStepContent,
FormStepHeading,
FormStepInputs,
FormStepRow,
} from "./components/form-step";
import { Select } from "./components/input/select";
import { TextInput } from "./components/input/text-input";
import { NumberInput } from "./components/input/number-input";
import { NextButton } from "./components/buttons/next-button";
import { BackButton } from "./components/buttons/back-button";
import { MultiStep } from "./multi-step";
export type Values = [Form<object>, Form<object>, Form<object>, Form<object>];
export type Params = {
fields: Fields;
onChange: (fields: Partial<Fields>) => void;
};
export type Fields = {
name: string;
surname: string;
age: number;
gender: string;
country: string;
};
export const schema: Schema<Values, object, Params> = [
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="nameSurname"
defaultValues={{
name: params.fields.name,
surname: params.fields.surname,
}}
resolver={zodResolver(
z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
)}
onChange={params.onChange}
>
<FormStepContent>
<FormStepHeading>What is your name?</FormStepHeading>
<FormStepInputs>
<FormStepRow>
<TextInput name="name" label="Name" placeholder="Your name" />
<TextInput
name="surname"
label="Surname"
placeholder="Your surname"
/>
</FormStepRow>
</FormStepInputs>
<NextButton>Next</NextButton>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="age"
defaultValues={{ age: params.fields.age }}
resolver={zodResolver(
z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
)}
onChange={params.onChange}
>
<FormStepContent>
<FormStepHeading>What is your age?</FormStepHeading>
<FormStepInputs>
<NumberInput name="age" label="Age" placeholder="Your age" />
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="gender"
defaultValues={{ gender: params.fields.gender }}
resolver={zodResolver(
z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<FormStepContent>
<FormStepHeading>What is your gender?</FormStepHeading>
<FormStepInputs>
<Select
name="gender"
label="Gender"
options={[
{ value: "", label: "Select your gender" },
{ value: "man", label: "Man" },
{ value: "woman", label: "Woman" },
{ value: "other", label: "Other" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="country"
defaultValues={{ country: params.fields.country }}
resolver={zodResolver(
z.object({
country: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<FormStepContent>
<FormStepHeading>What is your country?</FormStepHeading>
<FormStepInputs>
<Select
name="country"
label="Country"
options={[
{ value: "", label: "Select your country" },
{ value: "spain", label: "Spain" },
{ value: "france", label: "France" },
{ value: "germany", label: "Germany" },
{ value: "italy", label: "Italy" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
];
Then, we can update the app.tsx
file as you can see here.
// app.tsx
import { useState, useCallback } from "react";
import { Formity } from "@formity/react";
import { schema, type Values, type Params, type Fields } from "./schema";
const initialFields: Fields = {
name: "",
surname: "",
age: 0,
gender: "",
country: "",
};
export default function App() {
const [fields, setFields] = useState(initialFields);
const onChange = useCallback(
(values: Partial<Fields>) => {
setFields({ ...fields, ...values });
},
[fields],
);
return (
<Formity<Values, object, Params>
schema={schema}
params={{ fields, onChange }}
/>
);
}
Submit form
We need to pass an onSubmit
function using the params
prop of Formity
. It will be called on the last step, and to do it we need to update schema.tsx
as shown below.
// schema.tsx
import type { Schema, Form } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormStep,
FormStepContent,
FormStepHeading,
FormStepInputs,
FormStepRow,
} from "./components/form-step";
import { Select } from "./components/input/select";
import { TextInput } from "./components/input/text-input";
import { NumberInput } from "./components/input/number-input";
import { NextButton } from "./components/buttons/next-button";
import { BackButton } from "./components/buttons/back-button";
import { MultiStep } from "./multi-step";
export type Values = [Form<object>, Form<object>, Form<object>, Form<object>];
export type Params = {
fields: Fields;
onChange: (fields: Partial<Fields>) => void;
onSubmit: () => void;
};
export type Fields = {
name: string;
surname: string;
age: number;
gender: string;
country: string;
};
export const schema: Schema<Values, object, Params> = [
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="nameSurname"
defaultValues={{
name: params.fields.name,
surname: params.fields.surname,
}}
resolver={zodResolver(
z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
)}
onChange={params.onChange}
>
<FormStepContent>
<FormStepHeading>What is your name?</FormStepHeading>
<FormStepInputs>
<FormStepRow>
<TextInput name="name" label="Name" placeholder="Your name" />
<TextInput
name="surname"
label="Surname"
placeholder="Your surname"
/>
</FormStepRow>
</FormStepInputs>
<NextButton>Next</NextButton>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="age"
defaultValues={{ age: params.fields.age }}
resolver={zodResolver(
z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
)}
onChange={params.onChange}
>
<FormStepContent>
<FormStepHeading>What is your age?</FormStepHeading>
<FormStepInputs>
<NumberInput name="age" label="Age" placeholder="Your age" />
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="gender"
defaultValues={{ gender: params.fields.gender }}
resolver={zodResolver(
z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<FormStepContent>
<FormStepHeading>What is your gender?</FormStepHeading>
<FormStepInputs>
<Select
name="gender"
label="Gender"
options={[
{ value: "", label: "Select your gender" },
{ value: "man", label: "Man" },
{ value: "woman", label: "Woman" },
{ value: "other", label: "Other" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onBack }) => (
<MultiStep onNext={params.onSubmit} onBack={onBack}>
<FormStep
key="country"
defaultValues={{ country: params.fields.country }}
resolver={zodResolver(
z.object({
country: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<FormStepContent>
<FormStepHeading>What is your country?</FormStepHeading>
<FormStepInputs>
<Select
name="country"
label="Country"
options={[
{ value: "", label: "Select your country" },
{ value: "spain", label: "Spain" },
{ value: "france", label: "France" },
{ value: "germany", label: "Germany" },
{ value: "italy", label: "Italy" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
];
Then, we can update the app.tsx
file as you can see here.
// app.tsx
import { useState, useCallback } from "react";
import { Formity } from "@formity/react";
import { schema, type Values, type Params, type Fields } from "./schema";
import { Output } from "./components/output";
const initialFields: Fields = {
name: "",
surname: "",
age: 0,
gender: "",
country: "",
};
export default function App() {
const [fields, setFields] = useState(initialFields);
const [submit, setSubmit] = useState(false);
const onChange = useCallback(
(values: Partial<Fields>) => {
setFields({ ...fields, ...values });
},
[fields],
);
const onSubmit = useCallback(() => {
setSubmit(true);
}, []);
if (submit) {
return (
<Output
output={fields}
onStart={() => {
setSubmit(false);
setFields(initialFields);
}}
/>
);
}
return (
<Formity<Values, object, Params>
schema={schema}
params={{ fields, onChange, onSubmit }}
/>
);
}
Steps component
We'll create a Steps
component used to navigate to specific steps, and we will start by creating a components/steps.tsx
file with the following code.
// components/steps.tsx
import { tv } from "tailwind-variants";
interface StepsProps {
steps: {
label: string;
}[];
selected: number;
}
export function Steps({ steps, selected }: StepsProps) {
return (
<div className="fixed right-4 top-5 z-10 flex gap-3">
{steps.map((step, index) => (
<Step key={index} label={step.label} selected={index === selected} />
))}
</div>
);
}
const step = tv({
base: "flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-neutral-800 text-sm text-white ring-offset-2 ring-offset-neutral-950 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-white/10",
variants: {
selected: {
false: "bg-neutral-950",
true: "bg-neutral-800 ring-2 ring-neutral-800",
},
},
});
interface StepProps {
label: string;
selected: boolean;
}
function Step({ label, selected }: StepProps) {
return (
<button type="button" className={step({ selected })}>
{label}
</button>
);
}
Then, we'll update schema.tsx
to include the Steps
component.
// schema.tsx
import type { Schema, Form } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormStep,
FormStepContent,
FormStepHeading,
FormStepInputs,
FormStepRow,
} from "./components/form-step";
import { Steps } from "./components/steps";
import { Select } from "./components/input/select";
import { TextInput } from "./components/input/text-input";
import { NumberInput } from "./components/input/number-input";
import { NextButton } from "./components/buttons/next-button";
import { BackButton } from "./components/buttons/back-button";
import { MultiStep } from "./multi-step";
export type Values = [Form<object>, Form<object>, Form<object>, Form<object>];
export type Params = {
fields: Fields;
onChange: (fields: Partial<Fields>) => void;
onSubmit: () => void;
};
export type Fields = {
name: string;
surname: string;
age: number;
gender: string;
country: string;
};
const steps: { label: string }[] = [
{ label: "1" },
{ label: "2" },
{ label: "3" },
{ label: "4" },
];
export const schema: Schema<Values, object, Params> = [
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="nameSurname"
defaultValues={{
name: params.fields.name,
surname: params.fields.surname,
}}
resolver={zodResolver(
z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={0} />
<FormStepContent>
<FormStepHeading>What is your name?</FormStepHeading>
<FormStepInputs>
<FormStepRow>
<TextInput name="name" label="Name" placeholder="Your name" />
<TextInput
name="surname"
label="Surname"
placeholder="Your surname"
/>
</FormStepRow>
</FormStepInputs>
<NextButton>Next</NextButton>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="age"
defaultValues={{ age: params.fields.age }}
resolver={zodResolver(
z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={1} />
<FormStepContent>
<FormStepHeading>What is your age?</FormStepHeading>
<FormStepInputs>
<NumberInput name="age" label="Age" placeholder="Your age" />
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="gender"
defaultValues={{ gender: params.fields.gender }}
resolver={zodResolver(
z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={2} />
<FormStepContent>
<FormStepHeading>What is your gender?</FormStepHeading>
<FormStepInputs>
<Select
name="gender"
label="Gender"
options={[
{ value: "", label: "Select your gender" },
{ value: "man", label: "Man" },
{ value: "woman", label: "Woman" },
{ value: "other", label: "Other" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onBack }) => (
<MultiStep onNext={params.onSubmit} onBack={onBack}>
<FormStep
key="country"
defaultValues={{ country: params.fields.country }}
resolver={zodResolver(
z.object({
country: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={3} />
<FormStepContent>
<FormStepHeading>What is your country?</FormStepHeading>
<FormStepInputs>
<Select
name="country"
label="Country"
options={[
{ value: "", label: "Select your country" },
{ value: "spain", label: "Spain" },
{ value: "france", label: "France" },
{ value: "germany", label: "Germany" },
{ value: "italy", label: "Italy" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
];
Completed steps
The Steps
component should indicate what are the steps that have been completed. To do it, we'll use validation rules to check what are the completed steps.
We'll update Steps
to include a check
property in the object of the steps
array.
// components/steps.tsx
import type { ZodType } from "zod";
import { tv } from "tailwind-variants";
import type { Fields } from "@/schema";
interface StepsProps {
steps: {
label: string;
check: ZodType;
}[];
selected: number;
fields: Fields;
}
export function Steps({ steps, selected, fields }: StepsProps) {
return (
<div className="fixed right-4 top-5 z-10 flex gap-3">
{steps.map((step, index) => (
<Step
key={index}
label={step.label}
check={step.check}
selected={index === selected}
fields={fields}
/>
))}
</div>
);
}
const step = tv({
base: "flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-neutral-800 bg-neutral-950 text-sm text-white ring-offset-2 ring-offset-neutral-950 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-white/10",
variants: {
selected: {
true: "bg-neutral-800 ring-2 ring-neutral-800",
},
check: {
true: "bg-neutral-800",
},
},
});
interface StepProps {
label: string;
check: ZodType;
selected: boolean;
fields: Fields;
}
function Step({ label, check, selected, fields }: StepProps) {
return (
<button
type="button"
className={step({ selected, check: check.safeParse(fields).success })}
>
{label}
</button>
);
}
Then, we'll update schema.tsx
to include the validation rules in the steps
array.
// schema.tsx
import type { Schema, Form } from "@formity/react";
import type { ZodType } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormStep,
FormStepContent,
FormStepHeading,
FormStepInputs,
FormStepRow,
} from "./components/form-step";
import { Steps } from "./components/steps";
import { Select } from "./components/input/select";
import { TextInput } from "./components/input/text-input";
import { NumberInput } from "./components/input/number-input";
import { NextButton } from "./components/buttons/next-button";
import { BackButton } from "./components/buttons/back-button";
import { MultiStep } from "./multi-step";
export type Values = [Form<object>, Form<object>, Form<object>, Form<object>];
export type Params = {
fields: Fields;
onChange: (fields: Partial<Fields>) => void;
onSubmit: () => void;
};
export type Fields = {
name: string;
surname: string;
age: number;
gender: string;
country: string;
};
const steps: { label: string; check: ZodType }[] = [
{
label: "1",
check: z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
},
{
label: "2",
check: z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
},
{
label: "3",
check: z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
},
{
label: "4",
check: z.object({
country: z.string().nonempty({ message: "Required" }),
}),
},
];
export const schema: Schema<Values, object, Params> = [
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="nameSurname"
defaultValues={{
name: params.fields.name,
surname: params.fields.surname,
}}
resolver={zodResolver(
z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={0} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your name?</FormStepHeading>
<FormStepInputs>
<FormStepRow>
<TextInput name="name" label="Name" placeholder="Your name" />
<TextInput
name="surname"
label="Surname"
placeholder="Your surname"
/>
</FormStepRow>
</FormStepInputs>
<NextButton>Next</NextButton>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="age"
defaultValues={{ age: params.fields.age }}
resolver={zodResolver(
z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={1} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your age?</FormStepHeading>
<FormStepInputs>
<NumberInput name="age" label="Age" placeholder="Your age" />
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="gender"
defaultValues={{ gender: params.fields.gender }}
resolver={zodResolver(
z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={2} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your gender?</FormStepHeading>
<FormStepInputs>
<Select
name="gender"
label="Gender"
options={[
{ value: "", label: "Select your gender" },
{ value: "man", label: "Man" },
{ value: "woman", label: "Woman" },
{ value: "other", label: "Other" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onBack }) => (
<MultiStep onNext={params.onSubmit} onBack={onBack}>
<FormStep
key="country"
defaultValues={{ country: params.fields.country }}
resolver={zodResolver(
z.object({
country: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={3} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your country?</FormStepHeading>
<FormStepInputs>
<Select
name="country"
label="Country"
options={[
{ value: "", label: "Select your country" },
{ value: "spain", label: "Spain" },
{ value: "france", label: "France" },
{ value: "germany", label: "Germany" },
{ value: "italy", label: "Italy" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
];
Jump to steps
To jump to steps, we need to use the setState
function. We'll access this function using the Context API, so we'll need to update the files in the multi-step
folder.
multi-step/multi-step-value.ts
:
// multi-step/multi-step-value.ts
import type { OnNext, OnBack, SetState } from "@formity/react";
export interface MultiStepValue {
onNext: OnNext;
onBack: OnBack;
setState: SetState;
}
multi-step/multi-step.tsx
:
// multi-step/multi-step.tsx
import type { ReactNode } from "react";
import type { OnNext, OnBack, SetState } from "@formity/react";
import { useMemo } from "react";
import { MultiStepContext } from "./multi-step-context";
interface MultiStepProps {
onNext: OnNext;
onBack: OnBack;
setState: SetState;
children: ReactNode;
}
export function MultiStep({
onNext,
onBack,
setState,
children,
}: MultiStepProps) {
const values = useMemo(
() => ({ onNext, onBack, setState }),
[onNext, onBack, setState],
);
return (
<MultiStepContext.Provider value={values}>
{children}
</MultiStepContext.Provider>
);
}
Then, we'll update Steps
to include a state
property in the object of the steps
array. When we click a step, the setState
will be called with the corresponding state.
// components/steps.tsx
import type { ZodType } from "zod";
import type { State } from "@formity/react";
import { tv } from "tailwind-variants";
import type { Fields } from "@/schema";
import { useMultiStep } from "@/multi-step";
interface StepsProps {
steps: {
label: string;
check: ZodType;
state: State;
}[];
selected: number;
fields: Fields;
}
export function Steps({ steps, selected, fields }: StepsProps) {
return (
<div className="fixed right-4 top-5 z-10 flex gap-3">
{steps.map((step, index) => (
<Step
key={index}
label={step.label}
check={step.check}
state={step.state}
selected={index === selected}
fields={fields}
/>
))}
</div>
);
}
const step = tv({
base: "flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-neutral-800 bg-neutral-950 text-sm text-white ring-offset-2 ring-offset-neutral-950 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-white/10",
variants: {
selected: {
true: "bg-neutral-800 ring-2 ring-neutral-800",
},
check: {
true: "bg-neutral-800",
},
},
});
interface StepProps {
label: string;
check: ZodType;
state: State;
selected: boolean;
fields: Fields;
}
function Step({ label, check, state, selected, fields }: StepProps) {
const { setState } = useMultiStep();
return (
<button
type="button"
onClick={() => setState(state)}
className={step({ selected, check: check.safeParse(fields).success })}
>
{label}
</button>
);
}
After that, we'll update schema.tsx
to include the states in the steps
array and to pass the setState
function to the MultiStep
component.
// schema.tsx
import type { Schema, Form, State } from "@formity/react";
import type { ZodType } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormStep,
FormStepContent,
FormStepHeading,
FormStepInputs,
FormStepRow,
} from "./components/form-step";
import { Steps } from "./components/steps";
import { Select } from "./components/input/select";
import { TextInput } from "./components/input/text-input";
import { NumberInput } from "./components/input/number-input";
import { NextButton } from "./components/buttons/next-button";
import { BackButton } from "./components/buttons/back-button";
import { MultiStep } from "./multi-step";
export type Values = [Form<object>, Form<object>, Form<object>, Form<object>];
export type Params = {
fields: Fields;
onChange: (fields: Partial<Fields>) => void;
onSubmit: () => void;
};
export type Fields = {
name: string;
surname: string;
age: number;
gender: string;
country: string;
};
const steps: { label: string; check: ZodType; state: State }[] = [
{
label: "1",
check: z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
state: {
points: [{ path: [{ type: "list", slot: 0 }], values: {} }],
inputs: { type: "list", list: [] },
},
},
{
label: "2",
check: z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
state: {
points: [
{ path: [{ type: "list", slot: 0 }], values: {} },
{ path: [{ type: "list", slot: 1 }], values: {} },
],
inputs: { type: "list", list: [] },
},
},
{
label: "3",
check: z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
state: {
points: [
{ path: [{ type: "list", slot: 0 }], values: {} },
{ path: [{ type: "list", slot: 1 }], values: {} },
{ path: [{ type: "list", slot: 2 }], values: {} },
],
inputs: { type: "list", list: [] },
},
},
{
label: "4",
check: z.object({
country: z.string().nonempty({ message: "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: [] },
},
},
];
export const schema: Schema<Values, object, Params> = [
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack, setState }) => (
<MultiStep onNext={onNext} onBack={onBack} setState={setState}>
<FormStep
key="nameSurname"
defaultValues={{
name: params.fields.name,
surname: params.fields.surname,
}}
resolver={zodResolver(
z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={0} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your name?</FormStepHeading>
<FormStepInputs>
<FormStepRow>
<TextInput name="name" label="Name" placeholder="Your name" />
<TextInput
name="surname"
label="Surname"
placeholder="Your surname"
/>
</FormStepRow>
</FormStepInputs>
<NextButton>Next</NextButton>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack, setState }) => (
<MultiStep onNext={onNext} onBack={onBack} setState={setState}>
<FormStep
key="age"
defaultValues={{ age: params.fields.age }}
resolver={zodResolver(
z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={1} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your age?</FormStepHeading>
<FormStepInputs>
<NumberInput name="age" label="Age" placeholder="Your age" />
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack, setState }) => (
<MultiStep onNext={onNext} onBack={onBack} setState={setState}>
<FormStep
key="gender"
defaultValues={{ gender: params.fields.gender }}
resolver={zodResolver(
z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={2} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your gender?</FormStepHeading>
<FormStepInputs>
<Select
name="gender"
label="Gender"
options={[
{ value: "", label: "Select your gender" },
{ value: "man", label: "Man" },
{ value: "woman", label: "Woman" },
{ value: "other", label: "Other" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onBack, setState }) => (
<MultiStep onNext={params.onSubmit} onBack={onBack} setState={setState}>
<FormStep
key="country"
defaultValues={{ country: params.fields.country }}
resolver={zodResolver(
z.object({
country: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={3} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your country?</FormStepHeading>
<FormStepInputs>
<Select
name="country"
label="Country"
options={[
{ value: "", label: "Select your country" },
{ value: "spain", label: "Spain" },
{ value: "france", label: "France" },
{ value: "germany", label: "Germany" },
{ value: "italy", label: "Italy" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
];
Block submit
Lastly, we need to disable the button on the last step when there are uncompleted steps. To do that, we'll update the schema.tsx
file as shown below.
// schema.tsx
import type { Schema, Form, State } from "@formity/react";
import type { ZodType } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormStep,
FormStepContent,
FormStepHeading,
FormStepInputs,
FormStepRow,
} from "./components/form-step";
import { Steps } from "./components/steps";
import { Select } from "./components/input/select";
import { TextInput } from "./components/input/text-input";
import { NumberInput } from "./components/input/number-input";
import { NextButton } from "./components/buttons/next-button";
import { BackButton } from "./components/buttons/back-button";
import { MultiStep } from "./multi-step";
export type Values = [Form<object>, Form<object>, Form<object>, Form<object>];
export type Params = {
fields: Fields;
onChange: (fields: Partial<Fields>) => void;
onSubmit: () => void;
};
export type Fields = {
name: string;
surname: string;
age: number;
gender: string;
country: string;
};
const steps: { label: string; check: ZodType; state: State }[] = [
{
label: "1",
check: z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
state: {
points: [{ path: [{ type: "list", slot: 0 }], values: {} }],
inputs: { type: "list", list: [] },
},
},
{
label: "2",
check: z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
state: {
points: [
{ path: [{ type: "list", slot: 0 }], values: {} },
{ path: [{ type: "list", slot: 1 }], values: {} },
],
inputs: { type: "list", list: [] },
},
},
{
label: "3",
check: z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
state: {
points: [
{ path: [{ type: "list", slot: 0 }], values: {} },
{ path: [{ type: "list", slot: 1 }], values: {} },
{ path: [{ type: "list", slot: 2 }], values: {} },
],
inputs: { type: "list", list: [] },
},
},
{
label: "4",
check: z.object({
country: z.string().nonempty({ message: "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: [] },
},
},
];
export const schema: Schema<Values, object, Params> = [
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack, setState }) => (
<MultiStep onNext={onNext} onBack={onBack} setState={setState}>
<FormStep
key="nameSurname"
defaultValues={{
name: params.fields.name,
surname: params.fields.surname,
}}
resolver={zodResolver(
z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={0} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your name?</FormStepHeading>
<FormStepInputs>
<FormStepRow>
<TextInput name="name" label="Name" placeholder="Your name" />
<TextInput
name="surname"
label="Surname"
placeholder="Your surname"
/>
</FormStepRow>
</FormStepInputs>
<NextButton>Next</NextButton>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack, setState }) => (
<MultiStep onNext={onNext} onBack={onBack} setState={setState}>
<FormStep
key="age"
defaultValues={{ age: params.fields.age }}
resolver={zodResolver(
z.object({
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={1} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your age?</FormStepHeading>
<FormStepInputs>
<NumberInput name="age" label="Age" placeholder="Your age" />
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onNext, onBack, setState }) => (
<MultiStep onNext={onNext} onBack={onBack} setState={setState}>
<FormStep
key="gender"
defaultValues={{ gender: params.fields.gender }}
resolver={zodResolver(
z.object({
gender: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={2} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your gender?</FormStepHeading>
<FormStepInputs>
<Select
name="gender"
label="Gender"
options={[
{ value: "", label: "Select your gender" },
{ value: "man", label: "Man" },
{ value: "woman", label: "Woman" },
{ value: "other", label: "Other" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({}),
render: ({ params, onBack, setState }) => (
<MultiStep onNext={params.onSubmit} onBack={onBack} setState={setState}>
<FormStep
key="country"
defaultValues={{ country: params.fields.country }}
resolver={zodResolver(
z.object({
country: z.string().nonempty({ message: "Required" }),
}),
)}
onChange={params.onChange}
>
<Steps steps={steps} selected={3} fields={params.fields} />
<FormStepContent>
<FormStepHeading>What is your country?</FormStepHeading>
<FormStepInputs>
<Select
name="country"
label="Country"
options={[
{ value: "", label: "Select your country" },
{ value: "spain", label: "Spain" },
{ value: "france", label: "France" },
{ value: "germany", label: "Germany" },
{ value: "italy", label: "Italy" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton
disabled={steps.some((step) => {
return !step.check.safeParse(params.fields).success;
})}
>
Submit
</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
];