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.