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.
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.