Guides
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 follow along, start by cloning the repository below, which contains the code we'll use as the starting point.
Terminal
git clone https://github.com/martiserra99/formity-react-hook-form
Then install the dependencies.
Terminal
npm install
Conditional fields
To add a conditional field, start by creating the following component.
TSX
// 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.
TSX
// components/form/item/index.tsx
import { ConditionView, type Condition } from "./condition";
import { ColumnsView, type Columns } from "./columns";
import { InputView, type Input } from "./input";
import { NumberView, type Number } from "./number";
import { SelectView, type Select } from "./select";
import { TextareaView, type Textarea } from "./textarea";
import { MultiSelectView, type MultiSelect } from "./multi-select";
export type Item =
| Condition
| Columns
| Input
| Number
| Select
| Textarea
| MultiSelect;
export function ItemView(item: Item) {
switch (item.type) {
case "condition": {
return <ConditionView {...item} />;
}
case "columns": {
return <ColumnsView {...item} />;
}
case "input": {
return <InputView {...item} />;
}
case "number": {
return <NumberView {...item} />;
}
case "select": {
return <SelectView {...item} />;
}
case "textarea": {
return <TextareaView {...item} />;
}
case "multi-select": {
return <MultiSelectView {...item} />;
}
}
}
Then, we can update the flow with the code shown below.
TSX
// 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 type { Status, FormStatus } from "./types/status";
import { Form } from "./components/form";
import { Done } from "./components/done";
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: {
status: FormStatus;
};
};
const flow: Flow<Schema> = [
{
form: {
fields: () => ({
working: ["no", []],
company: ["", []],
}),
render: ({ fields, params, 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: "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}
status={params.status}
/>
),
},
},
{
variables: ({ working, company }) => ({
company: working === "yes" ? company : null,
}),
},
{
return: ({ working, company }) => ({
working,
company,
}),
},
];
export default function App() {
const [status, setStatus] = useState<Status<ReturnOutput<Schema>>>({
type: "form",
submitting: false,
});
const onReturn = useCallback<OnReturn<Schema>>(async (output) => {
setStatus({ type: "form", submitting: true });
// Show output in the console
console.log(output);
// Simulate a network request
await new Promise((resolve) => setTimeout(resolve, 2000));
setStatus({ type: "done", output });
}, []);
if (status.type === "done") {
return (
<Done
output={status.output}
onStartOver={() => setStatus({ type: "form", submitting: false })}
/>
);
}
return (
<Formity<Schema> flow={flow} params={{ status }} 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.