Getting started
Tutorial
Follow this tutorial to grasp the core concepts of Formity and how it has to be used.
Initial steps
In this tutorial, we'll show you how to turn a basic single-step form into a dynamic multi-step form with conditional logic. The starting point is already set up in the GitHub repository below, so go ahead and clone it to follow along.
git clone https://github.com/martiserra99/formity-tutorial
Make sure you run the following command to install all the dependencies.
npm install
This tutorial explains how to use Formity with TypeScript, but if you want to learn how to use it with JavaScript you can still follow this tutorial since almost everything is the same. The only thing that is different is that in JavaScript you don't define the types.
Single-step form
If you take a look at the app.tsx file, you'll find a single-step form already in place. This form is built using React Hook Form. However, you're not restricted to this library. Formity is designed to work smoothly with any single-step form library you choose.
// app.tsx
import { useCallback, useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form } from "./components/form";
import { Output } from "./components/output";
type Values = { name: string; surname: string; age: number };
export default function App() {
const [output, setOutput] = useState<Values | null>(null);
const onSubmit = useCallback((output: Values) => {
setOutput(output);
}, []);
if (output) {
return <Output output={output} onStart={() => setOutput(null)} />;
}
return (
<Form
defaultValues={{
name: "",
surname: "",
age: 20,
}}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
submit="Submit"
onSubmit={onSubmit}
/>
);
}
Multi-step form
To get started with Formity, the first thing we'll do is use the Formity component. It is the one that renders the multi-step form, and these are the most important props:
flow: Defines the structure and behavior of the multi-step form.onReturn: A callback function that is triggered when the form is completed.
We'll replace the code that we have in app.tsx with the following code.
// app.tsx
import { useCallback, useState } from "react";
import {
Formity,
type Flow,
type OnReturn,
type ReturnOutput,
} from "@formity/react";
import { Output } from "./components/output";
type Schema = {
render: React.ReactNode;
struct: [];
inputs: Record<never, never>;
params: Record<never, never>;
};
const flow: Flow<Schema> = [];
export default function App() {
const [output, setOutput] = useState<ReturnOutput<Schema> | null>(null);
const onReturn = useCallback<OnReturn<Schema>>((output) => {
setOutput(output);
}, []);
if (output) {
return <Output output={output} onStart={() => setOutput(null)} />;
}
return <Formity<Schema> flow={flow} onReturn={onReturn} />;
}
The next step is to create the flow, which defines the structure and behavior of the multi-step form. We'll do this by writting the following code.
// app.tsx
import { useCallback, useState } from "react";
import {
Formity,
type s,
type Flow,
type OnReturn,
type ReturnOutput,
} from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form } from "./components/form";
import { Output } from "./components/output";
type Schema = {
render: React.ReactNode;
struct: [
s.Form<{ name: string; surname: string; age: number }>,
s.Form<{ softwareDeveloper: string }>,
];
inputs: Record<never, never>;
params: Record<never, never>;
};
const flow: Flow<Schema> = [
{
form: {
fields: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ fields, onNext }) => (
<Form
key="yourself"
defaultValues={fields}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
submit="Next"
onSubmit={onNext}
/>
),
},
},
{
form: {
fields: () => ({
softwareDeveloper: ["", []],
}),
render: ({ fields, onNext }) => (
<Form
key="softwareDeveloper"
defaultValues={fields}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string().nonempty("Required"),
}),
)}
heading="Are you a software developer?"
content={[
{
type: "select",
name: "softwareDeveloper",
label: "Software Developer",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
],
},
]}
submit="Next"
onSubmit={onNext}
/>
),
},
},
];
export default function App() {
const [output, setOutput] = useState<ReturnOutput<Schema> | null>(null);
const onReturn = useCallback<OnReturn<Schema>>((output) => {
setOutput(output);
}, []);
if (output) {
return <Output output={output} onStart={() => setOutput(null)} />;
}
return <Formity<Schema> flow={flow} onReturn={onReturn} />;
}
The flow constant is an array of type Flow. There are different types of elements you can use within the flow, and in this example, we've included two form elements.
Additionally, to ensure complete type safety, the Flow accepts a Schema type that defines the structure and values handled at each step.
If you complete the multi-step form, you'll see that the onReturn callback is not called. That's because we need to add a return element to the flow, as shown below.
// app.tsx
import { useCallback, useState } from "react";
import {
Formity,
type s,
type Flow,
type OnReturn,
type ReturnOutput,
} from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form } from "./components/form";
import { Output } from "./components/output";
type Schema = {
render: React.ReactNode;
struct: [
s.Form<{ name: string; surname: string; age: number }>,
s.Form<{ softwareDeveloper: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: string;
}>,
];
inputs: Record<never, never>;
params: Record<never, never>;
};
const flow: Flow<Schema> = [
{
form: {
fields: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ fields, onNext }) => (
<Form
key="yourself"
defaultValues={fields}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
submit="Next"
onSubmit={onNext}
/>
),
},
},
{
form: {
fields: () => ({
softwareDeveloper: ["", []],
}),
render: ({ fields, onNext }) => (
<Form
key="softwareDeveloper"
defaultValues={fields}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string().nonempty("Required"),
}),
)}
heading="Are you a software developer?"
content={[
{
type: "select",
name: "softwareDeveloper",
label: "Software Developer",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
],
},
]}
submit="Next"
onSubmit={onNext}
/>
),
},
},
{
return: ({ name, surname, age, softwareDeveloper }) => ({
name,
surname,
age,
softwareDeveloper,
}),
},
];
export default function App() {
const [output, setOutput] = useState<ReturnOutput<Schema> | null>(null);
const onReturn = useCallback<OnReturn<Schema>>((output) => {
setOutput(output);
}, []);
if (output) {
return <Output output={output} onStart={() => setOutput(null)} />;
}
return <Formity<Schema> flow={flow} onReturn={onReturn} />;
}
So far, we've covered the form and return elements. However, Formity supports additional elements that allow you to build any logic you need. One of these is the condition element, which can be used as you can see here.
// app.tsx
import { useCallback, useState } from "react";
import {
Formity,
type s,
type Flow,
type OnReturn,
type ReturnOutput,
} from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form } from "./components/form";
import { Output } from "./components/output";
type Schema = {
render: React.ReactNode;
struct: [
s.Form<{ name: string; surname: string; age: number }>,
s.Form<{ softwareDeveloper: string }>,
s.Condition<{
then: [
s.Form<{ expertise: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: true;
expertise: string;
}>,
];
else: [
s.Form<{ interested: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: false;
interested: string;
}>,
];
}>,
];
inputs: Record<never, never>;
params: Record<never, never>;
};
const flow: Flow<Schema> = [
{
form: {
fields: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ fields, onNext }) => (
<Form
key="yourself"
defaultValues={fields}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
submit="Next"
onSubmit={onNext}
/>
),
},
},
{
form: {
fields: () => ({
softwareDeveloper: ["", []],
}),
render: ({ fields, onNext }) => (
<Form
key="softwareDeveloper"
defaultValues={fields}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string().nonempty("Required"),
}),
)}
heading="Are you a software developer?"
content={[
{
type: "select",
name: "softwareDeveloper",
label: "Software Developer",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
],
},
]}
submit="Next"
onSubmit={onNext}
/>
),
},
},
{
condition: {
if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
then: [
{
form: {
fields: () => ({
expertise: ["", []],
}),
render: ({ fields, onNext }) => (
<Form
key="expertise"
defaultValues={fields}
resolver={zodResolver(
z.object({
expertise: z.string().nonempty("Required"),
}),
)}
heading="What is your area of expertise?"
content={[
{
type: "select",
name: "expertise",
label: "Expertise",
placeholder: "Select an option",
options: [
{ value: "frontend", label: "Frontend development" },
{ value: "backend", label: "Backend development" },
{ value: "mobile", label: "Mobile development" },
],
},
]}
submit="Submit"
onSubmit={onNext}
/>
),
},
},
{
return: ({ name, surname, age, expertise }) => ({
name,
surname,
age,
softwareDeveloper: true,
expertise,
}),
},
],
else: [
{
form: {
fields: () => ({
interested: ["", []],
}),
render: ({ fields, onNext }) => (
<Form
key="interested"
defaultValues={fields}
resolver={zodResolver(
z.object({
interested: z.string().nonempty("Required"),
}),
)}
heading="Are you interested in learning how to code?"
content={[
{
type: "select",
name: "interested",
label: "Interested",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes, I am interested." },
{ value: "no", label: "No, it is not for me." },
{ value: "maybe", label: "Maybe, I am not sure." },
],
},
]}
submit="Submit"
onSubmit={onNext}
/>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
];
export default function App() {
const [output, setOutput] = useState<ReturnOutput<Schema> | null>(null);
const onReturn = useCallback<OnReturn<Schema>>((output) => {
setOutput(output);
}, []);
if (output) {
return <Output output={output} onStart={() => setOutput(null)} />;
}
return <Formity<Schema> flow={flow} onReturn={onReturn} />;
}
We also need to include the logic to navigate to previous steps. To enable this functionality we have to update the Form component as shown below.
// components/form/index.tsx
import type { DefaultValues, Resolver } from "react-hook-form";
import type { OnBack, OnNext } from "@formity/react";
import { useForm, FormProvider } from "react-hook-form";
import { ItemView, type Item } from "./item";
interface FormProps<T extends Record<string, unknown>> {
defaultValues: DefaultValues<T>;
resolver: Resolver<T>;
heading: string;
content: Item[];
buttons: {
back: string | null;
next: string;
};
onBack: OnBack<T>;
onNext: OnNext<T>;
}
export function Form<T extends Record<string, unknown>>({
defaultValues,
resolver,
heading,
content,
buttons,
onBack,
onNext,
}: FormProps<T>) {
const form = useForm({ defaultValues, resolver });
return (
<form
onSubmit={form.handleSubmit(onNext)}
className="color-scheme-dark flex h-screen w-full items-center justify-center px-4 py-8"
autoComplete="off"
>
<FormProvider {...form}>
<div className="w-full max-w-md">
<h2 className="mb-6 text-center text-4xl font-semibold text-white">
{heading}
</h2>
<div className="mb-6 flex flex-col gap-4">
{content.map((field, index) => (
<ItemView key={index} {...field} />
))}
</div>
<div className="flex gap-4">
{buttons.back && (
<button
type="button"
onClick={() => onBack(form.getValues())}
className="bg-neutral-90 w-full rounded-xl border border-neutral-800 px-6 py-2 text-base font-medium text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white active:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
>
{buttons.back}
</button>
)}
<button
type="submit"
className="w-full rounded-xl border border-transparent bg-blue-500 px-6 py-2 text-base font-medium text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white active:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
>
{buttons.next}
</button>
</div>
</div>
</FormProvider>
</form>
);
}
Then, we need to update the flow as shown below.
import { useCallback, useState } from "react";
import {
Formity,
type s,
type Flow,
type OnReturn,
type ReturnOutput,
} from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form } from "./components/form";
import { Output } from "./components/output";
type Schema = {
render: React.ReactNode;
struct: [
s.Form<{ name: string; surname: string; age: number }>,
s.Form<{ softwareDeveloper: string }>,
s.Condition<{
then: [
s.Form<{ expertise: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: true;
expertise: string;
}>,
];
else: [
s.Form<{ interested: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: false;
interested: string;
}>,
];
}>,
];
inputs: Record<never, never>;
params: Record<never, never>;
};
const flow: Flow<Schema> = [
{
form: {
fields: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ fields, onBack, onNext }) => (
<Form
key="yourself"
defaultValues={fields}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
buttons={{
back: null,
next: "Next",
}}
onBack={onBack}
onNext={onNext}
/>
),
},
},
{
form: {
fields: () => ({
softwareDeveloper: ["", []],
}),
render: ({ fields, onBack, onNext }) => (
<Form
key="softwareDeveloper"
defaultValues={fields}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string().nonempty("Required"),
}),
)}
heading="Are you a software developer?"
content={[
{
type: "select",
name: "softwareDeveloper",
label: "Software Developer",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
],
},
]}
buttons={{
back: "Back",
next: "Next",
}}
onBack={onBack}
onNext={onNext}
/>
),
},
},
{
condition: {
if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
then: [
{
form: {
fields: () => ({
expertise: ["", []],
}),
render: ({ fields, onBack, onNext }) => (
<Form
key="expertise"
defaultValues={fields}
resolver={zodResolver(
z.object({
expertise: z.string().nonempty("Required"),
}),
)}
heading="What is your area of expertise?"
content={[
{
type: "select",
name: "expertise",
label: "Expertise",
placeholder: "Select an option",
options: [
{ value: "frontend", label: "Frontend development" },
{ value: "backend", label: "Backend development" },
{ value: "mobile", label: "Mobile development" },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={onBack}
onNext={onNext}
/>
),
},
},
{
return: ({ name, surname, age, expertise }) => ({
name,
surname,
age,
softwareDeveloper: true,
expertise,
}),
},
],
else: [
{
form: {
fields: () => ({
interested: ["", []],
}),
render: ({ fields, onBack, onNext }) => (
<Form
key="interested"
defaultValues={fields}
resolver={zodResolver(
z.object({
interested: z.string().nonempty("Required"),
}),
)}
heading="Are you interested in learning how to code?"
content={[
{
type: "select",
name: "interested",
label: "Interested",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes, I am interested." },
{ value: "no", label: "No, it is not for me." },
{ value: "maybe", label: "Maybe, I am not sure." },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={onBack}
onNext={onNext}
/>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
];
export default function App() {
const [output, setOutput] = useState<ReturnOutput<Schema> | null>(null);
const onReturn = useCallback<OnReturn<Schema>>((output) => {
setOutput(output);
}, []);
if (output) {
return <Output output={output} onStart={() => setOutput(null)} />;
}
return <Formity<Schema> flow={flow} onReturn={onReturn} />;
}
We've successfully created a multi-step form with conditional logic. Be sure to explore the other flow elements to see everything Formity can do.
Submit form
Until now, we have shown the values we want to submit. Now, we need to create the logic to submit these values and keep track of the submission state. To do this, we'll start by creating a types/status.ts file with the following code.
// types/status.ts
export type Status = FormStatus | SubmittedStatus;
export type FormStatus = {
type: "form";
submitting: boolean;
};
export type SubmittedStatus = {
type: "submitted";
};
Then, we'll update the app.tsx file to include the submission logic and display a thank you screen once the values have been successfully submitted.
// app.tx
import { useCallback, useState } from "react";
import { Formity, type s, type Flow, type OnReturn } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { Status } from "./types/status";
import { Form } from "./components/form";
import { Submitted } from "./components/submitted";
type Schema = {
render: React.ReactNode;
struct: [
s.Form<{ name: string; surname: string; age: number }>,
s.Form<{ softwareDeveloper: string }>,
s.Condition<{
then: [
s.Form<{ expertise: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: true;
expertise: string;
}>,
];
else: [
s.Form<{ interested: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: false;
interested: string;
}>,
];
}>,
];
inputs: Record<never, never>;
params: Record<never, never>;
};
const flow: Flow<Schema> = [
{
form: {
fields: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ fields, onBack, onNext }) => (
<Form
key="yourself"
defaultValues={fields}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
buttons={{
back: null,
next: "Next",
}}
onBack={onBack}
onNext={onNext}
/>
),
},
},
{
form: {
fields: () => ({
softwareDeveloper: ["", []],
}),
render: ({ fields, onBack, onNext }) => (
<Form
key="softwareDeveloper"
defaultValues={fields}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string().nonempty("Required"),
}),
)}
heading="Are you a software developer?"
content={[
{
type: "select",
name: "softwareDeveloper",
label: "Software Developer",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
],
},
]}
buttons={{
back: "Back",
next: "Next",
}}
onBack={onBack}
onNext={onNext}
/>
),
},
},
{
condition: {
if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
then: [
{
form: {
fields: () => ({
expertise: ["", []],
}),
render: ({ fields, onBack, onNext }) => (
<Form
key="expertise"
defaultValues={fields}
resolver={zodResolver(
z.object({
expertise: z.string().nonempty("Required"),
}),
)}
heading="What is your area of expertise?"
content={[
{
type: "select",
name: "expertise",
label: "Expertise",
placeholder: "Select an option",
options: [
{ value: "frontend", label: "Frontend development" },
{ value: "backend", label: "Backend development" },
{ value: "mobile", label: "Mobile development" },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={onBack}
onNext={onNext}
/>
),
},
},
{
return: ({ name, surname, age, expertise }) => ({
name,
surname,
age,
softwareDeveloper: true,
expertise,
}),
},
],
else: [
{
form: {
fields: () => ({
interested: ["", []],
}),
render: ({ fields, onBack, onNext }) => (
<Form
key="interested"
defaultValues={fields}
resolver={zodResolver(
z.object({
interested: z.string().nonempty("Required"),
}),
)}
heading="Are you interested in learning how to code?"
content={[
{
type: "select",
name: "interested",
label: "Interested",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes, I am interested." },
{ value: "no", label: "No, it is not for me." },
{ value: "maybe", label: "Maybe, I am not sure." },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={onBack}
onNext={onNext}
/>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
];
export default function App() {
const [status, setStatus] = useState<Status>({
type: "form",
submitting: false,
});
const onReturn = useCallback<OnReturn<Schema>>(async (output) => {
setStatus({ type: "form", submitting: true });
// Show output in the console
console.log(output);
// Simulate a network request
await new Promise((resolve) => setTimeout(resolve, 2000));
setStatus({ type: "submitted" });
}, []);
if (status.type === "submitted") {
return (
<Submitted
onStart={() => setStatus({ type: "form", submitting: false })}
/>
);
}
return <Formity<Schema> flow={flow} onReturn={onReturn} />;
}
As you may have noticed, the form stays interactive during submission, with no feedback shown. To address this, we'll access the status object within the Form to disable the buttons and provide feedback to the user.
// components/form/index.tsx
import type { DefaultValues, Resolver } from "react-hook-form";
import type { OnBack, OnNext } from "@formity/react";
import { useForm, FormProvider } from "react-hook-form";
import type { FormStatus } from "@/types/status";
import { ItemView, type Item } from "./item";
interface FormProps<T extends Record<string, unknown>> {
defaultValues: DefaultValues<T>;
resolver: Resolver<T>;
heading: string;
content: Item[];
buttons: {
back: string | null;
next: string;
};
onBack: OnBack<T>;
onNext: OnNext<T>;
status: FormStatus;
}
export function Form<T extends Record<string, unknown>>({
defaultValues,
resolver,
heading,
content,
buttons,
onBack,
onNext,
status,
}: FormProps<T>) {
const form = useForm({ defaultValues, resolver });
return (
<form
onSubmit={form.handleSubmit(onNext)}
className="color-scheme-dark flex h-screen w-full items-center justify-center px-4 py-8"
autoComplete="off"
>
<FormProvider {...form}>
<div className="w-full max-w-md">
<h2 className="mb-6 text-center text-4xl font-semibold text-white">
{heading}
</h2>
<div className="mb-6 flex flex-col gap-4">
{content.map((field, index) => (
<ItemView key={index} {...field} />
))}
</div>
<div className="flex gap-4">
{buttons.back && (
<button
type="button"
disabled={status.submitting}
onClick={() => onBack(form.getValues())}
className="bg-neutral-90 w-full rounded-xl border border-neutral-800 px-6 py-2 text-base font-medium text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white active:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
>
{buttons.back}
</button>
)}
<button
type="submit"
disabled={status.submitting}
className="w-full rounded-xl border border-transparent bg-blue-500 px-6 py-2 text-base font-medium text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white active:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
>
{status.submitting ? "Submitting..." : buttons.next}
</button>
</div>
</div>
</FormProvider>
</form>
);
}
Then, we'll pass the status object to the flow so that we can provide it to each Form.
// app.tsx
import { useCallback, useState } from "react";
import { Formity, type s, type Flow, type OnReturn } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { Status, FormStatus } from "./types/status";
import { Form } from "./components/form";
import { Submitted } from "./components/submitted";
type Schema = {
render: React.ReactNode;
struct: [
s.Form<{ name: string; surname: string; age: number }>,
s.Form<{ softwareDeveloper: string }>,
s.Condition<{
then: [
s.Form<{ expertise: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: true;
expertise: string;
}>,
];
else: [
s.Form<{ interested: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: false;
interested: string;
}>,
];
}>,
];
inputs: Record<never, never>;
params: {
status: FormStatus;
};
};
const flow: Flow<Schema> = [
{
form: {
fields: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ fields, params, onBack, onNext }) => (
<Form
key="yourself"
defaultValues={fields}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
buttons={{
back: null,
next: "Next",
}}
onBack={onBack}
onNext={onNext}
status={params.status}
/>
),
},
},
{
form: {
fields: () => ({
softwareDeveloper: ["", []],
}),
render: ({ fields, params, onBack, onNext }) => (
<Form
key="softwareDeveloper"
defaultValues={fields}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string().nonempty("Required"),
}),
)}
heading="Are you a software developer?"
content={[
{
type: "select",
name: "softwareDeveloper",
label: "Software Developer",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
],
},
]}
buttons={{
back: "Back",
next: "Next",
}}
onBack={onBack}
onNext={onNext}
status={params.status}
/>
),
},
},
{
condition: {
if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
then: [
{
form: {
fields: () => ({
expertise: ["", []],
}),
render: ({ fields, params, onBack, onNext }) => (
<Form
key="expertise"
defaultValues={fields}
resolver={zodResolver(
z.object({
expertise: z.string().nonempty("Required"),
}),
)}
heading="What is your area of expertise?"
content={[
{
type: "select",
name: "expertise",
label: "Expertise",
placeholder: "Select an option",
options: [
{ value: "frontend", label: "Frontend development" },
{ value: "backend", label: "Backend development" },
{ value: "mobile", label: "Mobile development" },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={onBack}
onNext={onNext}
status={params.status}
/>
),
},
},
{
return: ({ name, surname, age, expertise }) => ({
name,
surname,
age,
softwareDeveloper: true,
expertise,
}),
},
],
else: [
{
form: {
fields: () => ({
interested: ["", []],
}),
render: ({ fields, params, onBack, onNext }) => (
<Form
key="interested"
defaultValues={fields}
resolver={zodResolver(
z.object({
interested: z.string().nonempty("Required"),
}),
)}
heading="Are you interested in learning how to code?"
content={[
{
type: "select",
name: "interested",
label: "Interested",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes, I am interested." },
{ value: "no", label: "No, it is not for me." },
{ value: "maybe", label: "Maybe, I am not sure." },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={onBack}
onNext={onNext}
status={params.status}
/>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
];
export default function App() {
const [status, setStatus] = useState<Status>({
type: "form",
submitting: false,
});
const onReturn = useCallback<OnReturn<Schema>>(async (output) => {
setStatus({ type: "form", submitting: true });
// Show output in the console
console.log(output);
// Simulate a network request
await new Promise((resolve) => setTimeout(resolve, 2000));
setStatus({ type: "submitted" });
}, []);
if (status.type === "submitted") {
return (
<Submitted
onStart={() => setStatus({ type: "form", submitting: false })}
/>
);
}
return (
<Formity<Schema> flow={flow} params={{ status }} onReturn={onReturn} />
);
}
Next steps
You've successfully completed the tutorial. To dive deeper into Formity's capabilities, you can continue with the following sections.
Important: The upcoming sections use a different setup. Do not combine them with the tutorial code. Instead, clone the following Github repository.
git clone https://github.com/martiserra99/formity-docs
This codebase is similar to the one in this tutorial, but without the submission logic.