Advanced conceptsConditional fields

Advanced concepts

Conditional fields

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


Conditional fields

We can create a field that appears when a condition is met. To do it, we can start by creating the following component.

// components/form/item/condition.tsx
import { useFormContext } from "react-hook-form";

import { ItemView, type Item } from ".";

export interface Condition {
  type: "condition";
  if: (values: unknown) => boolean;
  watch: string[];
  items: Item[];
}

export function ConditionView(props: Condition) {
  const { watch } = useFormContext();
  const variables = watch(props.watch).reduce(
    (acc, value, index) => ({ ...acc, [props.watch[index]]: value }),
    {},
  );
  if (props.if(variables)) {
    return props.items.map((item, index) => <ItemView key={index} {...item} />);
  }
  return null;
}

We also need to update the following file to include this new component.

// components/form/item/index.tsx
import { ColumnsView, type Columns } from "./columns";
import { ConditionView, type Condition } from "./condition";
import { InputView, type Input } from "./input";
import { NumberView, type Number } from "./number";
import { SelectView, type Select } from "./select";
import { TextareaView, type Textarea } from "./textarea";

export type Item = Columns | Condition | Input | Number | Select | Textarea;

export function ItemView(item: Item) {
  switch (item.type) {
    case "columns": {
      return <ColumnsView {...item} />;
    }
    case "condition": {
      return <ConditionView {...item} />;
    }
    case "input": {
      return <InputView {...item} />;
    }
    case "number": {
      return <NumberView {...item} />;
    }
    case "select": {
      return <SelectView {...item} />;
    }
    case "textarea": {
      return <TextareaView {...item} />;
    }
  }
}

Then, we can update the flow with the code 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<{ working: string; company: string }>,
    s.Variables<{ company: string | null }>,
    s.Return<{ working: string; company: string | null }>,
  ];
  inputs: Record<never, never>;
  params: Record<never, never>;
};

const flow: Flow<Schema> = [
  {
    form: {
      fields: () => ({
        working: ["no", []],
        company: ["", []],
      }),
      render: ({ fields, onBack, onNext }) => (
        <Form
          key="yourself"
          defaultValues={fields}
          resolver={zodResolver(
            z
              .object({
                working: z.string(),
                company: z.string(),
              })
              .superRefine((data, ctx) => {
                if (data.working === "yes") {
                  if (data.company === "") {
                    ctx.addIssue({
                      code: z.ZodIssueCode.custom,
                      message: "Required",
                      path: ["company"],
                    });
                  }
                }
              }),
          )}
          heading="Tell us about yourself"
          content={[
            {
              type: "select",
              name: "working",
              label: "Are you working?",
              placeholder: "Select an option",
              options: [
                { value: "yes", label: "Yes" },
                { value: "no", label: "No" },
              ],
            },
            {
              type: "condition",
              if: ({ working }: { working: string }) => working === "yes",
              watch: ["working"],
              items: [
                {
                  type: "input",
                  name: "company",
                  label: "At what company?",
                  placeholder: "Company name",
                },
              ],
            },
          ]}
          buttons={{
            back: null,
            next: "Submit",
          }}
          onBack={onBack}
          onNext={onNext}
        />
      ),
    },
  },
  {
    variables: ({ working, company }) => ({
      company: working === "yes" ? company : null,
    }),
  },
  {
    return: ({ working, company }) => ({
      working,
      company,
    }),
  },
];

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 need to apply validation rules to the conditional field only when its condition is met. Additionally, we should set a default value for the field when it’s hidden, using a variable.