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

  • useWatch 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 ,
	,
	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  = .(, {
	: [
		{
			: "section",
			: "identity",
			: "Learning cohort",
			: 2,
			: [
				{
					: "field",
					: "identity.title",
					: "text",
					: "Cohort title",
					: "full",
				},
				{
					: "field",
					: "identity.discipline",
					: "select",
					: "Discipline",
					: [
						{ : "writing", : "Writing" },
						{ : "data", : "Data practice" },
						{ : "craft", : "Material craft" },
						{ : "leadership", : "Leadership" },
					],
				},
				{
					: "field",
					: "identity.level",
					: "select",
					: "Level",
					: [
						{ : "foundation", : "Foundation" },
						{ : "intermediate", : "Intermediate" },
						{ : "advanced", : "Advanced" },
					],
				},
				{
					: "field",
					: "identity.durationWeeks",
					: "number",
					: "Duration in weeks",
					: { : 1, : 52, : 1 },
				},
			],
		},
		{
			: "array",
			: "sessionFormats",
			: "Session formats",
			: "Model different seating and mentor configurations.",
			: { : "seminar", : 8, : 1 },
			: [
				{
					: "field",
					: "format",
					: "select",
					: "Format",
					: [
						{ : "seminar", : "Seminar" },
						{ : "studio", : "Studio" },
						{ : "clinic", : "Clinic" },
						{ : "critique", : "Critique" },
					],
				},
				{
					: "field",
					: "cohortSize",
					: "number",
					: "Learners",
					: { : 2, : 120, : 1 },
				},
				{
					: "field",
					: "mentorCount",
					: "number",
					: "Mentors",
					: { : 1, : 20, : 1 },
				},
			],
		},
		{
			: "array",
			: "priceBands",
			: "Per-capacity pricing",
			: { : 1, : 1, : 0 },
			: [
				{
					: "field",
					: "minimumSeats",
					: "number",
					: "Minimum seats",
					: { : 1, : 1 },
				},
				{
					: "field",
					: "maximumSeats",
					: "number",
					: "Maximum seats",
					: { : 1, : 1 },
				},
				{
					: "field",
					: "pricePerLearner",
					: "number",
					: "Price per learner",
					: { : 0, : 10 },
				},
			],
		},
		{
			: "section",
			: "media",
			: "Media",
			: [
				{
					: "field",
					: "media.cover",
					: "file",
					: "Cover image",
					: { : "image/*" },
				},
				{
					: "array",
					: "media.resources",
					: "Resource gallery",
					: { : "", : "" },
					: [
						{
							: "field",
							: "url",
							: "text",
							: "Resource URL",
						},
						{
							: "field",
							: "caption",
							: "text",
							: "Caption",
						},
					],
				},
			],
		},
		...(),
	],
})
 
function () {
	return [
		{
			: "section",
			: "offer-early",
			: "Early-bird offer",
			: 2,
			: [
				{
					: "field",
					: "offers.earlyBird.enabled",
					: "checkbox",
					: "Enabled",
				},
				{
					: "field",
					: "offers.earlyBird.percent",
					: "number",
					: "Reduction percent",
					: () => ...,
					: { : 1, : 80, : 1 },
				},
				{
					: "field",
					: "offers.earlyBird.deadline",
					: "date",
					: "Deadline",
					: () => ...,
				},
			],
		},
		{
			: "section",
			: "offer-team",
			: "Team offer",
			: 2,
			: [
				{
					: "field",
					: "offers.team.enabled",
					: "checkbox",
					: "Enabled",
				},
				{
					: "field",
					: "offers.team.minimumSeats",
					: "number",
					: "Minimum seats",
					: () => ...,
					: { : 2, : 1 },
				},
				{
					: "field",
					: "offers.team.percent",
					: "number",
					: "Reduction percent",
					: () => ...,
					: { : 1, : 80, : 1 },
				},
			],
		},
		{
			: "section",
			: "offer-scholarship",
			: "Scholarship offer",
			: 2,
			: [
				{
					: "field",
					: "offers.scholarship.enabled",
					: "checkbox",
					: "Enabled",
				},
				{
					: "field",
					: "offers.scholarship.reservedSeats",
					: "number",
					: "Reserved seats",
					: () => ...,
					: { : 1, : 1 },
				},
			],
		},
		{
			: "section",
			: "offer-alumni",
			: "Returning learner offer",
			: 2,
			: [
				{
					: "field",
					: "offers.alumni.enabled",
					: "checkbox",
					: "Enabled",
				},
				{
					: "field",
					: "offers.alumni.percent",
					: "number",
					: "Reduction percent",
					: () => ...,
					: { : 1, : 80, : 1 },
				},
			],
		},
	] satisfies readonly <, typeof .>[]
}
 
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.