Advanced concepts

Conditional fields

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


Initial steps

We'll show you how to add 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-advanced-concepts

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

npm install

Conditional field

To create a conditional field, we will create a file named conditional-field.tsx 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 function, form value names, and a field. It evaluates the condition using the form values and renders the field if true.

We can now update the schema.tsx file to use the conditional field.

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

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

import { Step, Layout, TextField, YesNo, NextButton } from "./components";

import { MultiStep } from "./multi-step";

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 }) => (
        <MultiStep onNext={onNext} onBack={onBack}>
          <Step
            key="working"
            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"],
                      });
                    }
                  }
                }),
            )}
          >
            <Layout
              heading="Tell us about yourself"
              description="We would want to know 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={<NextButton>Next</NextButton>}
            />
          </Step>
        </MultiStep>
      ),
    },
  },
  {
    variables: ({ working, company }) => ({
      company: working ? company : null,
    }),
  },
  {
    return: ({ working, company }) => ({
      working,
      company,
    }),
  },
];

For the conditional field, we need to apply validation rules only when the condition is met. Moreoever, we also define a variable to set a value when the field is hidden.