Advanced concepts

Conditional fields

Learn how to add fields that appear when a condition is met.


First steps

We can create fields that appear when a condition is met. To learn how to do it, clone the following Github repository so that you don't need to start from scratch.

git clone https://github.com/martiserra99/formity-react-docs

Make sure you run the following command to install all the dependencies:

npm install

Create conditional field

To create a conditional field, we will create a conditional-field.tsx file with the following component:

// conditional-field.tsx
import type { ReactNode } from "react";

import { useFormContext } from "react-hook-form";

interface ConditionalFieldProps<T extends object> {
  condition: (values: T) => boolean;
  values: string[];
  children: ReactNode;
}

export default function ConditionalField<T extends object>({
  condition,
  values,
  children,
}: ConditionalFieldProps<T>) {
  const { watch } = useFormContext();
  const variables = watch(values).reduce(
    (acc, value, index) => ({ ...acc, [values[index]]: value }),
    {},
  );
  if (condition(variables)) {
    return children;
  }
  return null;
}

This component receives a condition, the list of values of the form that we want to work with, and the field that we want to render conditionally.

Once the component has been created, we can create a schema.tsx file and use the component the following way:

// schema.tsx
import type { Schema, Form, Return, Variables } from "@formity/react";

import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

import { FormView, FormLayout, TextField, YesNo, Next } from "./components";

import { Controller } from "./controller";

import ConditionalField from "./conditional-field";

export type Values = [
  Form<{ working: boolean; company: string }>,
  Variables<{ company: string | null }>,
  Return<{ working: boolean; company: string | null }>,
];

export const schema: Schema<Values> = [
  {
    form: {
      values: () => ({
        working: [true, []],
        company: ["", []],
      }),
      render: ({ values, onNext, onBack, getState, setState }) => (
        <Controller
          step="working"
          onNext={onNext}
          onBack={onBack}
          getState={getState}
          setState={setState}
        >
          <FormView
            defaultValues={values}
            resolver={zodResolver(
              z
                .object({
                  working: z.boolean(),
                  company: z.string(),
                })
                .superRefine((data, ctx) => {
                  if (data.working) {
                    if (data.company === "") {
                      ctx.addIssue({
                        code: z.ZodIssueCode.custom,
                        message: "Required",
                        path: ["company"],
                      });
                    }
                  }
                }),
            )}
          >
            <FormLayout
              heading="Tell us about yourself"
              description="We would want to know a little bit more about you"
              fields={[
                <YesNo key="working" name="working" label="Working" />,
                <ConditionalField<{ working: boolean }>
                  key="company"
                  condition={({ working }) => working}
                  values={["working"]}
                >
                  <TextField key="company" name="company" label="Company" />
                </ConditionalField>,
              ]}
              button={<Next>Next</Next>}
            />
          </FormView>
        </Controller>
      ),
    },
  },
  {
    variables: ({ working, company }) => ({
      company: working ? company : null,
    }),
  },
  {
    return: ({ working, company }) => ({
      working,
      company,
    }),
  },
];

Apart from using the component to render the field conditionally, we also do some other things. We have the condition in the validation rules, and we also create a variable with a value that depends on the condition.

We now have to update the App.tsx file so that it contains the following code:

// 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} />;
}