Skip to content
Form, Please

Learning cohort editor

This form combines a loaded draft with session formats, price bands, media, and four enrollment offers. A fake title service returns suggestions for the current title.

Try the complete flow

Edit the title and choose a remote suggestion. Reorder session formats or create overlapping price bands. Enter Reserved cohort to see save-conflict feedback without losing the draft.

Loading cohort draft…

What this form demonstrates

  • form.api.watch() for a query key and a live preview.
  • form.api.setValue for an application-owned suggestion control.
  • Two generated arrays with cross-row schema validation.
  • Conditional offer fields that preserve hidden values.
  • Three sequential mutations from transformed submit output.

Copy the source

"use client"
 
import {
	,
	,
	,
	,
} from "@tanstack/react-query"
import {
	,
	type ,
	type ,
	,
} from "form-please"
import {  } from "form-please/default-slots"
import {  } from "form-please/native-controls"
import {  } from "react"
import {  } from "zod"
 
import {  } from "./query-to-resource"
 
const  = 
	.({
		: .({
			: .().(5, "Use a specific cohort title"),
			: .(["writing", "data", "craft", "leadership"]),
			: .(["foundation", "intermediate", "advanced"]),
			: .().().(1).(52),
		}),
		: 
			.(
				.({
					: .(["seminar", "studio", "clinic", "critique"]),
					: .().().(2).(120),
					: .().().(1).(20),
				}),
			)
			.(1, "Add at least one session format"),
		: 
			.(
				.({
					: .().().(1),
					: .().().(1),
					: .().(0),
				}),
			)
			.(1, "Add at least one price band"),
		: .({
			: 
				.<File | undefined>(
					() =>
						 ===  ||
						(typeof  !== "undefined" &&  instanceof ),
					"Choose an image file",
				)
				.(),
			: .(
				.({
					: .().("Enter a valid resource URL"),
					: .().(2, "Add a caption"),
				}),
			),
		}),
		: .({
			: .({
				: .(),
				: .().(1).(80).(),
				: .().(),
			}),
			: .({
				: .(),
				: .().().(2).(),
				: .().(1).(80).(),
			}),
			: .({
				: .(),
				: .().().(1).(),
			}),
			: .({
				: .(),
				: .().(1).(80).(),
			}),
		}),
	})
	.((, ) => {
		for (const [, ] of ..()) {
			if (. < .) {
				.({
					: "custom",
					: ["priceBands", , "maximumSeats"],
					: "Maximum seats must be at least the minimum",
				})
			}
		}
		const  = [....].(
			(, ) => . - .,
		)
		for (let  = 1;  < .;  += 1) {
			const  = [ - 1]
			const  = []
			if (
				 !==  &&
				 !==  &&
				. <= .
			) {
				.({
					: "custom",
					: ["priceBands"],
					: "Price bands cannot overlap",
				})
				break
			}
		}
 
		const : readonly [
			boolean,
			unknown,
			string,
			(string | number)[],
		][] = [
			[
				...,
				...,
				"Set the early-bird reduction",
				["offers", "earlyBird", "percent"],
			],
			[
				...,
				...,
				"Set the early-bird deadline",
				["offers", "earlyBird", "deadline"],
			],
			[
				...,
				...,
				"Set the team threshold",
				["offers", "team", "minimumSeats"],
			],
			[
				...,
				...,
				"Set the team reduction",
				["offers", "team", "percent"],
			],
			[
				...,
				...,
				"Set reserved scholarship seats",
				["offers", "scholarship", "reservedSeats"],
			],
			[
				...,
				...,
				"Set the alumni reduction",
				["offers", "alumni", "percent"],
			],
		]
		for (const [, , , ] of ) {
			if ( &&  === ) {
				.({ : "custom", ,  })
			}
		}
	})
	.(() => ({
		...,
		: ..(
			(, ) =>  + .,
			0,
		),
	}))
 
type  = <typeof >
type  = <typeof >
 
const  = {
	: {
		: "Field Notes for Better Decisions",
		: "leadership",
		: "intermediate",
		: 8,
	},
	: [
		{ : "seminar", : 18, : 2 },
		{ : "clinic", : 6, : 2 },
	],
	: [
		{ : 1, : 9, : 480 },
		{ : 10, : 24, : 420 },
	],
	: {
		: ,
		: [
			{
				: "https://example.test/resources/field-notes",
				: "Sample workbook",
			},
		],
	},
	: {
		: { : true, : 12, : "2027-02-15" },
		: { : true, : 6, : 10 },
		: { : true, : 3 },
		: { : false, :  },
	},
} satisfies 
 
const  = ({
	: (),
	: (),
})
 
function ({
	,
	,
}: {
	readonly : string
	readonly : (: string) => void
}) {
	const  = ({
		: ["cohort-title-suggestions", ],
		: () =>
			(
				[
					`${}: Practice Lab`,
					`${}: Guided Studio`,
					`${}: Working Sessions`,
				],
				300,
			),
		: .(). >= 5,
		: 20_000,
	})
	const  = ()
 
	return (
		<
			="form-please-complex__embedded"
			="Title suggestions"
		>
			<>Remote title suggestions</>
			< ="form-please-complex__choice-list">
				{(, {
					: ({  }) => {
						let  = "Generating suggestions…"
						if ( === "idle") {
							 = "Enter at least five characters."
						}
						return <>{}</>
					},
					: ({ ,  }) => (
						<>
							{. === "pending" && (
								<>Refreshing suggestions…</>
							)}
							{. === "paused" && (
								<>Refresh paused; showing saved suggestions.</>
							)}
							{. === "error" && (
								<>Refresh failed; showing saved suggestions.</>
							)}
							{.(() => (
								<
									={}
									={() => ()}
									="button"
								>
									{}
								</>
							))}
						</>
					),
					: () => <>Could not load title suggestions.</>,
				})}
			</>
		</>
	)
}
 
const  = .(, () => [
	.("identity", {
		: "Learning cohort",
		: 2,
		: [
			.("identity.title", {
				: "text",
				: "Cohort title",
				: "full",
			}),
			.("identity.discipline", {
				: "select",
				: "Discipline",
				: [
					{ : "writing", : "Writing" },
					{ : "data", : "Data practice" },
					{ : "craft", : "Material craft" },
					{ : "leadership", : "Leadership" },
				],
			}),
			.("identity.level", {
				: "select",
				: "Level",
				: [
					{ : "foundation", : "Foundation" },
					{ : "intermediate", : "Intermediate" },
					{ : "advanced", : "Advanced" },
				],
			}),
			.("identity.durationWeeks", {
				: "number",
				: "Duration in weeks",
				: { : 1, : 52, : 1 },
			}),
		],
	}),
	.("sessionFormats", {
		: "Session formats",
		: "Model different seating and mentor configurations.",
		: { : "seminar", : 8, : 1 },
		: () => [
			.("format", {
				: "select",
				: "Format",
				: [
					{ : "seminar", : "Seminar" },
					{ : "studio", : "Studio" },
					{ : "clinic", : "Clinic" },
					{ : "critique", : "Critique" },
				],
			}),
			.("cohortSize", {
				: "number",
				: "Learners",
				: { : 2, : 120, : 1 },
			}),
			.("mentorCount", {
				: "number",
				: "Mentors",
				: { : 1, : 20, : 1 },
			}),
		],
	}),
	.("priceBands", {
		: "Per-capacity pricing",
		: { : 1, : 1, : 0 },
		: () => [
			.("minimumSeats", {
				: "number",
				: "Minimum seats",
				: { : 1, : 1 },
			}),
			.("maximumSeats", {
				: "number",
				: "Maximum seats",
				: { : 1, : 1 },
			}),
			.("pricePerLearner", {
				: "number",
				: "Price per learner",
				: { : 0, : 10 },
			}),
		],
	}),
	.("media", {
		: "Media",
		: [
			.("media.cover", {
				: "file",
				: "Cover image",
				: { : "image/*" },
			}),
			.("media.resources", {
				: "Resource gallery",
				: { : "", : "" },
				: () => [
					.("url", {
						: "text",
						: "Resource URL",
					}),
					.("caption", {
						: "text",
						: "Caption",
					}),
				],
			}),
		],
	}),
 
	.("offer-early", {
		: "Early-bird offer",
		: 2,
		: [
			.("offers.earlyBird.enabled", {
				: "checkbox",
				: "Enabled",
			}),
			.("offers.earlyBird.percent", {
				: "number",
				: "Reduction percent",
				: () => ...,
				: { : 1, : 80, : 1 },
			}),
			.("offers.earlyBird.deadline", {
				: "date",
				: "Deadline",
				: () => ...,
			}),
		],
	}),
	.("offer-team", {
		: "Team offer",
		: 2,
		: [
			.("offers.team.enabled", {
				: "checkbox",
				: "Enabled",
			}),
			.("offers.team.minimumSeats", {
				: "number",
				: "Minimum seats",
				: () => ...,
				: { : 2, : 1 },
			}),
			.("offers.team.percent", {
				: "number",
				: "Reduction percent",
				: () => ...,
				: { : 1, : 80, : 1 },
			}),
		],
	}),
	.("offer-scholarship", {
		: "Scholarship offer",
		: 2,
		: [
			.("offers.scholarship.enabled", {
				: "checkbox",
				: "Enabled",
			}),
			.("offers.scholarship.reservedSeats", {
				: "number",
				: "Reserved seats",
				: () => ...,
				: { : 1, : 1 },
			}),
		],
	}),
	.("offer-alumni", {
		: "Returning learner offer",
		: 2,
		: [
			.("offers.alumni.enabled", {
				: "checkbox",
				: "Enabled",
			}),
			.("offers.alumni.percent", {
				: "number",
				: "Reduction percent",
				: () => ...,
				: { : 1, : 80, : 1 },
			}),
		],
	}),
])
 
export function () {
	const [] = (
		() => new ({ : { : { : false } } }),
	)
	return (
		< ={}>
			< />
		</>
	)
}
 
function () {
	const  = ({
		: ["learning-cohort-draft", "cohort-41"],
		: () => (, 410),
	})
	const  = ({
		: async (: ) => {
			await (, 380)
			if (...() === "reserved cohort")
				throw new ()
			return { : . + 100 }
		},
	})
	const  = ({
		: (: ) =>
			({ : ... }, 290),
	})
	const  = ({
		: (: ) =>
			(
				{
					: .(.).(() => .)
						.,
				},
				340,
			),
	})
	const [, ] = ("Draft loaded from the fake API.")
	const  = .(, {
		: ,
		async ({  }) {
			try {
				const  = await .()
				const  = await .()
				const  = await .()
				(
					`Revision ${.} saved with ${.} resource(s) and ${.} active offer(s).`,
				)
			} catch () {
				if ( instanceof ) {
					(
						"That title is already reserved. Choose a suggestion or edit it.",
					)
					return
				}
 
				("The cohort could not be saved. Your draft is intact.")
			}
		},
	})
 
	if (.)
		return (
			< ="form-please-complex">Loading cohort draft…</>
		)
	if (.)
		return (
			< ="form-please-complex">
				Could not load the cohort draft.
			</>
		)
	let  = 
	if (. || . || .) {
		 = "Synchronizing three resources…"
	}
	const  = ..()
	const  = ..(
		(, ) =>  + .,
		0,
	)
 
	return (
		<
			="Learning cohort editor example"
			="form-please-complex"
		>
			< ="form-please-complex__kicker">Async cohort editor</>
			< ="form-please-complex__summary">
				Remote title suggestions, movable configuration rows, band validation,
				media, four offer subforms, and conflict recovery remain typed end to
				end.
			</>
			<. ="form-please-complex__form" ={}>
				<
					={..}
					={() =>
						..("identity.title", )
					}
				/>
				< ="form-please-complex__actions">
					<. ="form-please-complex__primary">
						Save cohort
					</.>
					< ="polite">{}</>
				</>
			</.>
			<
				="Cohort preview"
				="form-please-complex__preview"
			>
				<>{..}</>
				<>
					{..} formats · {} aggregate seats
				</>
			</>
		</>
	)
}
 
class  extends  {}
 
function <>(: , : number): <> {
	return new (() =>
		.(() => (), ),
	)
}

Compare the other workflows in Examples.