Getting started
Tutorial
Follow this tutorial to grasp the core concepts of Formity and how it has to be used.
First steps
In this tutorial we will show you how to create a multi-step form with conditional logic. For that, you will need to 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
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.
Components
The first step when using Formity is to create the components for your multi-step form. You can use any form library you prefer, and the components
folder includes examples built with react-hook-form
.
Schema
After creating the components, we can create the schema, which determines the structure and behavior of the multi-step form. The schema uses the Schema
type and requires a corresponding Values
type to define the data collected and processed throughout the form.
Let’s create a schema.tsx
file with the following content:
// schema.tsx
import type { Schema, Form, Cond, Return } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormView,
FormLayout,
YesNo,
MultiSelect,
Listbox,
Next,
Back,
} from "./components";
import { Controller } from "./controller";
export type Values = [
Form<{ softwareDeveloper: boolean }>,
Cond<{
then: [
Form<{ languages: string[] }>,
Return<{
softwareDeveloper: boolean;
languages: string[];
}>,
];
else: [
Form<{ interested: string }>,
Return<{
softwareDeveloper: boolean;
interested: string;
}>,
];
}>,
];
export const schema: Schema<Values> = [
{
form: {
values: () => ({
softwareDeveloper: [true, []],
}),
render: ({ values, onNext, onBack, getFlow, setFlow }) => (
<Controller
step="softwareDeveloper"
onNext={onNext}
onBack={onBack}
getFlow={getFlow}
setFlow={setFlow}
>
<FormView
defaultValues={values}
resolver={zodResolver(
z.object({
softwareDeveloper: z.boolean(),
}),
)}
>
<FormLayout
heading="Are you a software developer?"
description="We would like to know if you are a software developer"
fields={[
<YesNo
key="softwareDeveloper"
name="softwareDeveloper"
label="Software Developer"
/>,
]}
button={<Next>Next</Next>}
/>
</FormView>
</Controller>
),
},
},
{
cond: {
if: ({ softwareDeveloper }) => softwareDeveloper,
then: [
{
form: {
values: () => ({
languages: [[], []],
}),
render: ({ values, onNext, onBack, getFlow, setFlow }) => (
<Controller
step="languages"
onNext={onNext}
onBack={onBack}
getFlow={getFlow}
setFlow={setFlow}
>
<FormView
defaultValues={values}
resolver={zodResolver(
z.object({
languages: z.array(z.string()),
}),
)}
>
<FormLayout
heading="What are your favourite programming languages?"
description="We would like to know which of the following programming languages you like the most"
fields={[
<MultiSelect
key="languages"
name="languages"
label="Languages"
options={[
{ value: "javascript", label: "JavaScript" },
{ value: "python", label: "Python" },
{ value: "go", label: "Go" },
]}
direction="y"
/>,
]}
button={<Next>Next</Next>}
back={<Back />}
/>
</FormView>
</Controller>
),
},
},
{
return: ({ softwareDeveloper, languages }) => ({
softwareDeveloper,
languages,
}),
},
],
else: [
{
form: {
values: () => ({
interested: ["maybe", []],
}),
render: ({ values, onNext, onBack, getFlow, setFlow }) => (
<Controller
step="interested"
onNext={onNext}
onBack={onBack}
getFlow={getFlow}
setFlow={setFlow}
>
<FormView
defaultValues={values}
resolver={zodResolver(
z.object({
interested: z.string(),
}),
)}
>
<FormLayout
heading="Would you be interested in learning how to code?"
description="Having coding skills can be very beneficial"
fields={[
<Listbox
key="interested"
name="interested"
label="Interested"
options={[
{
value: "maybe",
label: "Maybe in another time.",
},
{
value: "yes",
label: "Yes, that sounds good.",
},
{
value: "no",
label: "No, it is not for me.",
},
]}
/>,
]}
button={<Next>Next</Next>}
back={<Back />}
/>
</FormView>
</Controller>
),
},
},
{
return: ({ softwareDeveloper, interested }) => ({
softwareDeveloper,
interested,
}),
},
],
},
},
];
The Values
type is an array containing Form
, Cond
and Return
types, each representing the data handled at various stages of the form. This Values
type is then passed to the Schema
type, allowing the schema to define the overall structure and behavior of the multi-step form.
Controller
As we can see, we have a component called Controller
. This component receives as props the arguments of the render
function along a step
prop. If we take a look at the controller/controller.tsx
file we will see the following code:
// controller/controller.tsx
import type { ReactNode } from "react";
import type { OnNext, OnBack, GetFlow, SetFlow } from "@formity/react";
import { useMemo } from "react";
import { ControllerContext } from "./controller-context";
interface ControllerProps {
step: string;
onNext: OnNext;
onBack: OnBack;
getFlow: GetFlow;
setFlow: SetFlow;
children: ReactNode;
}
export function Controller({
step,
onNext,
onBack,
getFlow,
setFlow,
children,
}: ControllerProps) {
const values = useMemo(
() => ({
onNext,
onBack,
getFlow,
setFlow,
}),
[onNext, onBack, getFlow, setFlow],
);
return (
<ControllerContext.Provider value={values}>
<div key={step} className="h-full">
{children}
</div>
</ControllerContext.Provider>
);
}
The values from the render
function are passed to a context provider, allowing them to be accessed anywhere within the component tree. These values are specific to Formity and primarily enable navigation between steps.
Additionally, note the use of the key
prop with the value from the step
prop. Using the key
prop is essential to ensure the form state updates correctly each time you navigate to a different step.
Formity
Once the schema is defined we can use the Formity
component to render the form. The main props of this component are the following ones:
schema
: Defines the structure and behavior of the multi-step form.onReturn
: A callback function that is triggered when the form is completed.
Let's write the following in the App.tsx
file:
// App.tsx
import { useCallback, useState } from "react";
import { Formity, OnReturn, ReturnValues } from "@formity/react";
import { Data } from "./components";
import { schema, Values } from "./schema";
export default function App() {
const [values, setValues] = useState<ReturnValues<Values> | null>(null);
const onReturn = useCallback<OnReturn<Values>>((values) => {
setValues(values);
}, []);
if (values) {
return <Data data={values} onStart={() => setValues(null)} />;
}
return <Formity<Values> schema={schema} onReturn={onReturn} />;
}
As you can see, we have successfully created a multi-step form with conditional logic. Additionally, you may have noticed the automatic type inference for the return values, ensuring seamless integration and type safety.