Skip to content
Form, Please

Research grant application

The schema is a discriminated union for individual and collective applicants. Nested unions select the representation, settlement, and reporting fields.

Try the complete flow

Choose a collective and the registered representation path. Search the fake registry and apply a result with typed form.api updates. Change the payout route to resolve another conditional branch.

Branching application

Applicant identity, registry lookup, settlement route, reporting rules, and a two-request submission all share one typed form state.

Applicant

Choose the legal path before entering dependent details.

Proposed work

At least 80 characters; this becomes the public summary.

Settlement and reporting

No application sent yet.

What this form demonstrates

  • Nested discriminated unions for the applicant, the representation path, the payout route, and the reporting status.
  • One section for each representation path instead of a resolver on each field.
  • An application-owned registry search inside the form.
  • Read-only values populated through form.api.setValue.
  • form.api.watch() for conditional application UI and a live preview.
  • Two sequential mutations from transformed schema output.
  • Preserved values when the selected branch changes. The output contains only the selected branches.

Copy the source

"use client"
 
import {
	,
	,
	,
	,
} from "@tanstack/react-query"
import {
	,
	type ,
	type ,
	type ,
} from "form-please"
import {  } from "form-please/default-slots"
import {  } from "form-please/native-controls"
import {  } from "react"
import {  } from "zod"
 
const  = .(
	"path",
	[
		.({
			: .("registered"),
			: .({ : "Select a registry record" }),
			: .().(),
			: .().(),
		}),
		.({
			: .("forming"),
			: 
				.()
				.()
				.(2, "Enter the collective's working name")
				.(""),
			: .().(),
		}),
	],
	{ : "Choose how the collective is represented" },
)
 
const  = .("method", [
	.({
		: .("bank"),
		: 
			.()
			.()
			.(8, "Enter a valid settlement account")
			.(""),
	}),
	.({
		: .("digital-wallet"),
		: 
			.()
			.()
			.(3, "Enter a wallet handle")
			.(""),
	}),
])
 
const  = .("status", [
	.({
		: .("registered"),
		: 
			.()
			.()
			.(4, "Enter the reporting reference")
			.(""),
	}),
	.({ : .(["exempt", "pending"]) }),
])
 
const  = .({
	: .({
		: .().(2, "Enter the lead applicant's name"),
		: .().("Enter a valid email"),
	}),
	: .(["local", "international"]),
	: .({
		: .(["research", "public-program", "education"]),
		: .().(5, "Use a descriptive project title"),
		: .().(80, "Write at least 80 characters"),
		: .().(1_000).(250_000),
		: .().().(1).(36),
	}),
	: ,
	: ,
	: 
		.()
		.(
			() => ,
			"Confirm the application before sending it",
		),
})
 
const  = 
	.("applicantKind", [
		.({ : .("person") }),
		.({
			: .("collective"),
			: ,
		}),
	])
	.(() => ({
		...,
		: {
			....,
			: ...(),
		},
		: `${..}:${..}`,
	}))
 
type  = <typeof >
type  = <typeof >
 
type  = {
	readonly : string
	readonly : string
	readonly : string
}
 
const : readonly [] = [
	{ : "arc-104", : "Open Field Assembly", : "CA" },
	{ : "arc-208", : "Night School Cooperative", : "DE" },
	{ : "arc-319", : "Public Signal Workshop", : "NZ" },
]
 
// A starting draft for the collective variant. The representation path select
// shows its empty option until the user chooses a path.
const  = {
	: {},
} satisfies { : <.<typeof >> }
 
const  = {
	...,
	: "person",
	: { : "Mina Park", : "mina@example.test" },
	: "local",
	: {
		: "research",
		: "A public atlas of overlooked urban sounds",
		:
			"We will record, annotate, and publish an accessible field archive with neighborhood listening sessions and an open teaching kit.",
		: 42_000,
		: 9,
	},
	: { : "bank", : "SETTLE-482910" },
	: { : "pending" },
	: false,
} satisfies 
 
const  = ({
	: (),
	: (),
})
type  = <typeof >
 
function ({
	,
	,
}: {
	readonly : 
	readonly : string | undefined
}) {
	const [, ] = ("")
	const  = ({
		: ["grant-registry", ],
		: () =>
			(
				.(() =>
					..().(.().()),
				),
			),
		: 30_000,
	})
	return (
		<
			="form-please-complex__embedded"
			="Registry search"
		>
			<>
				Search the independent registry
				<
					={() => (..)}
					="Try Open or School"
					="search"
					={}
				/>
			</>
			< ="form-please-complex__choice-list">
				{. && <>Checking records…</>}
				{.?.(() => (
					<
						={ === .}
						={.}
						={() => {
							..("organization.registryId", .)
							..("organization.name", .)
							..(
								"organization.registrationCountry",
								.,
							)
						}}
						="button"
					>
						{.} · {.}
					</>
				))}
			</>
		</>
	)
}
 
const  = .(, () => [
	.("applicant", {
		: "Applicant",
		: "Choose the legal path before entering dependent details.",
		: 2,
		: [
			.("applicantKind", {
				: "select",
				: "Applying as",
				: [
					{ : "person", : "An individual" },
					{ : "collective", : "A collective" },
				],
			}),
			.("jurisdiction", {
				: "select",
				: "Administrative scope",
				: [
					{ : "local", : "Domestic" },
					{ : "international", : "Cross-border" },
				],
			}),
			.("contact.name", {
				: "text",
				: "Lead applicant",
				: true,
			}),
			.("contact.email", {
				: "text",
				: "Contact email",
				: { : "email" },
				: true,
			}),
		],
	}),
	.("collective", {
		: "Collective identity",
		: ({  }) =>  === "collective",
		: 2,
		: [
			.("organization.path", {
				: "select",
				: "Representation",
				: [
					{ : "registered", : "Registered collective" },
					{ : "forming", : "Collective in formation" },
				],
				: {
					: { : "Choose a path", : true },
				},
			}),
			.("registered-collective", {
				: 2,
				: "full",
				: () => () === "registered",
				: [
					.("organization.registryId", {
						: "text",
						: "Registry record ID",
						: true,
					}),
					.("organization.name", {
						: "registered-name",
						: "text",
						: "Registered name",
						: true,
					}),
					.("organization.registrationCountry", {
						: "registered-country",
						: "text",
						: "Registration country",
						: true,
					}),
				],
			}),
			.("forming-collective", {
				: 2,
				: "full",
				: () => () === "forming",
				: [
					.("organization.name", {
						: "working-name",
						: "text",
						: "Working name",
					}),
					.("organization.registrationCountry", {
						: "working-country",
						: "text",
						: "Registration country",
					}),
				],
			}),
		],
	}),
	.("project", {
		: "Proposed work",
		: 2,
		: [
			.("project.stream", {
				: "select",
				: "Funding stream",
				: [
					{ : "research", : "Independent research" },
					{ : "public-program", : "Public program" },
					{ : "education", : "Open education" },
				],
			}),
			.("project.title", {
				: "text",
				: "Project title",
				: true,
			}),
			.("project.abstract", {
				: "textarea",
				: "Abstract",
				: "At least 80 characters; this becomes the public summary.",
				: "full",
				: true,
				: { : 5 },
			}),
			.("project.requestedFunds", {
				: "number",
				: "Requested funds",
				: { : 1_000, : 250_000, : 500 },
			}),
			.("project.durationMonths", {
				: "number",
				: "Duration in months",
				: { : 1, : 36, : 1 },
			}),
		],
	}),
	.("settlement", {
		: "Settlement and reporting",
		: 2,
		: [
			.("payout.method", {
				: "select",
				: "Disbursement route",
				: [
					{ : "bank", : "Settlement account" },
					{ : "digital-wallet", : "Digital wallet" },
				],
			}),
			.("payout.bankAccount", {
				: "text",
				: "Settlement account",
				: ({  }) => . === "bank",
			}),
			.("payout.walletHandle", {
				: "text",
				: "Wallet handle",
				: ({  }) => . === "digital-wallet",
			}),
			.("reporting.status", {
				: "select",
				: "Reporting status",
				: [
					{ : "registered", : "Registered" },
					{ : "exempt", : "Exempt" },
					{ : "pending", : "Pending" },
				],
			}),
			.("reporting.reference", {
				: "text",
				: "Reporting reference",
				: ({  }) => . === "registered",
			}),
			.("confirmAccuracy", {
				: "checkbox",
				: "I confirm that the application is accurate",
				: "full",
			}),
		],
	}),
])
 
export function () {
	const [] = (
		() =>
			new ({
				: { : { : false } },
			}),
	)
 
	return (
		< ={}>
			< />
		</>
	)
}
 
function () {
	const [, ] = ("No application sent yet.")
	const  = ({
		: (: ) =>
			({ : ., : true }, 420),
	})
	const  = ({
		: (: ) =>
			({ : `grant-${..}-2048` }, 520),
	})
	const  = .(, {
		,
		async ({  }) {
			try {
				await .()
				const  = await .()
				(`Application ${.} passed preview and was sent.`)
			} catch {
				("The review service is unavailable. Your values are intact.")
			}
		},
	})
	let  = 
	if (.)  = "Sending application…"
	if (.)  = "Building preview…"
	const  = ..()
	let  = null
	if (
		. === "collective" &&
		.. === "registered"
	) {
		 = (
			<
				={}
				={..}
			/>
		)
	}
	let  = "Individual"
	if (. === "collective")  = "Collective"
 
	return (
		<
			="Research grant application example"
			="form-please-complex"
		>
			< ="form-please-complex__kicker">Branching application</>
			< ="form-please-complex__summary">
				Applicant identity, registry lookup, settlement route, reporting rules,
				and a two-request submission all share one typed form state.
			</>
			<. ="form-please-complex__form" ={}>
				{}
				<
					="Application preview"
					="form-please-complex__preview"
				>
					<>{.. || "Untitled application"}</>
					<>
						{}
						{" · "}${...()} ·{" "}
						{..} months
					</>
				</>
				< ="form-please-complex__actions">
					<. ="form-please-complex__primary">
						Preview and send
					</.>
					< ="polite">{}</>
				</>
			</.>
		</>
	)
}
 
function (: ) {
	if (. === "collective") return ..
	return 
}
 
function <>(: ,  = 280): <> {
	return new (() =>
		.(() => (), ),
	)
}

Compare the other workflows in Examples.