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.