Skip to content
Form, Please

Campaign builder

Seven campaign templates share an audience, schedule, channel, and payment model. Each template shows a different payload section.

Try the complete flow

Switch between the loaded draft and a new campaign. Change the template to see the definition resolve a different section. Paid templates also show amount and recurring interval fields.

Loading campaign builder…

What this form demonstrates

  • Conditional sections for seven schema branches.
  • Context-provided audience options.
  • Separate create and update mutations outside form state.
  • useWatch for the live campaign preview.
  • Transformed submit output with a derived channel list.
  • Preserved values when a conditional section becomes hidden.

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"
 
const  = [
	"newsletter",
	"product-launch",
	"event-invite",
	"fundraiser",
	"course-drop",
	"community-update",
	"feedback-pulse",
] as 
 
const  = 
	.({
		: .().(),
		: .().(4, "Name this campaign"),
		: .(),
		: .({
			: .().(1, "Choose an audience"),
			: .(["immediate", "scheduled", "rolling"]),
			: .({
				: .(),
				: .(),
				: .(),
			}),
		}),
		: .({
			: .().(1, "Choose a start date"),
			: .().(),
		}),
		: .({
			: .().(),
			: .().(),
		}),
		: .({
			: .().(),
			: .().(),
			: .().().(0).(),
			: .(["limited", "general", "preorder"]),
		}),
		: .({
			: .().(),
			: .().(),
			: .().().(1).(),
			: .(),
		}),
		: .({
			: .().(),
			: .().(1).(),
			: .().(1).(),
		}),
		: .({
			: .().(),
			: .().().(1).(),
			: .(),
		}),
		: .({
			: .().(),
			: .().(),
			: .().().(1).(),
		}),
		: .({
			: .().(),
			: .().().(1).(),
			: .(),
		}),
		: .({
			: .(["free", "fixed", "flexible", "recurring"]),
			: .().(1).(),
			: .(["USD", "EUR", "GBP"]),
			: .(["monthly", "annual"]),
		}),
	})
	.((, ) => {
		if (!.(..).()) {
			.({
				: "custom",
				: ["audience", "channels"],
				: "Choose at least one delivery channel",
			})
		}
		if (
			.. !== "immediate" &&
			.. !==  &&
			.. < ..
		) {
			.({
				: "custom",
				: ["schedule", "endsOn"],
				: "The end date must follow the start date",
			})
		}
 
		const : <readonly [unknown, (string | number)[], string]> = []
		switch (.) {
			case "newsletter":
				.(
					[
						..,
						["newsletter", "subject"],
						"Write the email subject",
					],
					[
						..,
						["newsletter", "preheader"],
						"Write the preheader",
					],
				)
				break
			case "product-launch":
				.(
					[
						..,
						["productLaunch", "productName"],
						"Name the product",
					],
					[
						..,
						["productLaunch", "sku"],
						"Enter the catalog code",
					],
					[
						..,
						["productLaunch", "initialStock"],
						"Set the opening stock",
					],
				)
				break
			case "event-invite":
				.(
					[
						..,
						["eventInvite", "eventName"],
						"Name the event",
					],
					[
						..,
						["eventInvite", "venue"],
						"Enter the venue",
					],
					[
						..,
						["eventInvite", "capacity"],
						"Set capacity",
					],
				)
				break
			case "fundraiser":
				.(
					[
						..,
						["fundraiser", "cause"],
						"Describe the cause",
					],
					[
						..,
						["fundraiser", "goalAmount"],
						"Set the funding goal",
					],
				)
				break
			case "course-drop":
				.(
					[
						..,
						["courseDrop", "courseTitle"],
						"Name the course",
					],
					[
						..,
						["courseDrop", "seatLimit"],
						"Set the seat limit",
					],
				)
				break
			case "community-update":
				.(
					[
						..,
						["communityUpdate", "topic"],
						"Describe the update topic",
					],
					[
						..,
						["communityUpdate", "moderator"],
						"Name the moderator",
					],
					[
						..,
						["communityUpdate", "responseWindowDays"],
						"Set the response window",
					],
				)
				break
			case "feedback-pulse":
				.(
					[
						..,
						["feedbackPulse", "question"],
						"Write the feedback question",
					],
					[
						..,
						["feedbackPulse", "responseLimit"],
						"Set the response limit",
					],
				)
				break
		}
		for (const [, , ] of ) {
			if ( ===  ||  === "") {
				.({ : "custom", ,  })
			}
		}
 
		if (
			(.) &&
			.. !== "free" &&
			.. === 
		) {
			.({
				: "custom",
				: ["payment", "amount"],
				: "Set an amount for this payment model",
			})
		}
	})
	.(() => ({
		...,
		: .(..)
			.(([, ]) => )
			.(([]) => ),
	}))
 
type  = <typeof >
type  = <typeof >
type  = {
	readonly : readonly {
		readonly : string
		readonly : string
	}[]
}
 
const  = {
	: { : , :  },
	: {
		: ,
		: ,
		: ,
		: "general",
	},
	: {
		: ,
		: ,
		: ,
		: true,
	},
	: {
		: ,
		: ,
		: ,
	},
	: {
		: ,
		: ,
		: true,
	},
	: {
		: ,
		: ,
		: ,
	},
	: {
		: ,
		: ,
		: true,
	},
} as 
 
const  = {
	: ,
	: "New community campaign",
	: "newsletter",
	: {
		: "active-members",
		: "scheduled",
		: { : true, : false, : true },
	},
	: { : "2027-03-10", : "2027-03-21" },
	...,
	: {
		: "What we are making this month",
		: "Three new ways to take part",
	},
	: {
		: "free",
		: ,
		: "USD",
		: "monthly",
	},
} satisfies 
 
const  = {
	...,
	: "campaign-204",
	: "Spring material fund",
	: "fundraiser",
	: { : , :  },
	: {
		: "Fund free access to the shared material library",
		: 18_000,
		: 35,
	},
	: {
		: "flexible",
		: 10,
		: "USD",
		: "monthly",
	},
} satisfies 
 
const  = ({
	: (),
	: (),
})
const  = .<>()
 
const  = .(, {
	: [
		{
			: "section",
			: "campaign",
			: "Campaign foundation",
			: 2,
			: [
				{
					: "field",
					: "name",
					: "text",
					: "Campaign name",
					: "full",
				},
				{
					: "field",
					: "template",
					: "select",
					: "Campaign template",
					: [
						{ : "newsletter", : "Newsletter" },
						{ : "product-launch", : "Product launch" },
						{ : "event-invite", : "Event invitation" },
						{ : "fundraiser", : "Fundraiser" },
						{ : "course-drop", : "Course release" },
						{ : "community-update", : "Community update" },
						{ : "feedback-pulse", : "Feedback pulse" },
					],
				},
				{
					: "field",
					: "audience.segmentId",
					: "select",
					: "Audience segment",
					: ({  }) => .,
				},
				{
					: "field",
					: "audience.deliveryMode",
					: "select",
					: "Delivery model",
					: [
						{ : "immediate", : "Immediate" },
						{ : "scheduled", : "Scheduled window" },
						{ : "rolling", : "Rolling audience entry" },
					],
				},
				{
					: "field",
					: "audience.channels.email",
					: "checkbox",
					: "Email",
				},
				{
					: "field",
					: "audience.channels.push",
					: "checkbox",
					: "Push",
				},
				{
					: "field",
					: "audience.channels.web",
					: "checkbox",
					: "Web inbox",
				},
				{
					: "field",
					: "schedule.startsOn",
					: "date",
					: "Starts on",
				},
				{
					: "field",
					: "schedule.endsOn",
					: "date",
					: "Ends on",
					: () => .. !== "immediate",
				},
			],
		},
		{
			: "section",
			: "newsletter",
			: "Newsletter content",
			: 2,
			: ({  }) =>  === "newsletter",
			: [
				{
					: "field",
					: "newsletter.subject",
					: "text",
					: "Subject",
				},
				{
					: "field",
					: "newsletter.preheader",
					: "text",
					: "Preheader",
				},
			],
		},
		{
			: "section",
			: "product-launch",
			: "Product launch",
			: 2,
			: ({  }) =>  === "product-launch",
			: [
				{
					: "field",
					: "productLaunch.productName",
					: "text",
					: "Product name",
				},
				{
					: "field",
					: "productLaunch.sku",
					: "text",
					: "Catalog code",
				},
				{
					: "field",
					: "productLaunch.initialStock",
					: "number",
					: "Opening stock",
					: { : 0, : 1 },
				},
				{
					: "field",
					: "productLaunch.releaseKind",
					: "select",
					: "Release kind",
					: [
						{ : "limited", : "Limited edition" },
						{ : "general", : "General release" },
						{ : "preorder", : "Preorder" },
					],
				},
			],
		},
		{
			: "section",
			: "event-invite",
			: "Event invitation",
			: 2,
			: ({  }) =>  === "event-invite",
			: [
				{
					: "field",
					: "eventInvite.eventName",
					: "text",
					: "Event name",
				},
				{
					: "field",
					: "eventInvite.venue",
					: "text",
					: "Venue",
				},
				{
					: "field",
					: "eventInvite.capacity",
					: "number",
					: "Capacity",
					: { : 1, : 1 },
				},
				{
					: "field",
					: "eventInvite.requiresRegistration",
					: "checkbox",
					: "Registration required",
				},
			],
		},
		{
			: "section",
			: "fundraiser",
			: "Fundraiser",
			: 2,
			: ({  }) =>  === "fundraiser",
			: [
				{
					: "field",
					: "fundraiser.cause",
					: "textarea",
					: "Cause",
					: "full",
					: { : 3 },
				},
				{
					: "field",
					: "fundraiser.goalAmount",
					: "number",
					: "Goal amount",
					: { : 1, : 100 },
				},
				{
					: "field",
					: "fundraiser.suggestedContribution",
					: "number",
					: "Suggested contribution",
					: { : 1, : 5 },
				},
			],
		},
		{
			: "section",
			: "course-drop",
			: "Course release",
			: 2,
			: ({  }) =>  === "course-drop",
			: [
				{
					: "field",
					: "courseDrop.courseTitle",
					: "text",
					: "Course title",
				},
				{
					: "field",
					: "courseDrop.seatLimit",
					: "number",
					: "Seat limit",
					: { : 1, : 1 },
				},
				{
					: "field",
					: "courseDrop.certificateIncluded",
					: "checkbox",
					: "Include certificate",
				},
			],
		},
		{
			: "section",
			: "community-update",
			: "Community update",
			: 2,
			: ({  }) =>  === "community-update",
			: [
				{
					: "field",
					: "communityUpdate.topic",
					: "textarea",
					: "Update topic",
					: "full",
					: { : 3 },
				},
				{
					: "field",
					: "communityUpdate.moderator",
					: "text",
					: "Moderator",
				},
				{
					: "field",
					: "communityUpdate.responseWindowDays",
					: "number",
					: "Response window in days",
					: { : 1, : 1 },
				},
			],
		},
		{
			: "section",
			: "feedback-pulse",
			: "Feedback pulse",
			: 2,
			: ({  }) =>  === "feedback-pulse",
			: [
				{
					: "field",
					: "feedbackPulse.question",
					: "textarea",
					: "Question",
					: "full",
					: { : 3 },
				},
				{
					: "field",
					: "feedbackPulse.responseLimit",
					: "number",
					: "Response limit",
					: { : 1, : 1 },
				},
				{
					: "field",
					: "feedbackPulse.anonymous",
					: "checkbox",
					: "Allow anonymous responses",
				},
			],
		},
		{
			: "section",
			: "payment",
			: "Payment model",
			: 2,
			: ({  }) => (),
			: [
				{
					: "field",
					: "payment.mode",
					: "select",
					: "Payment mode",
					: [
						{ : "free", : "Free" },
						{ : "fixed", : "Fixed" },
						{ : "flexible", : "Flexible contribution" },
						{ : "recurring", : "Recurring" },
					],
				},
				{
					: "field",
					: "payment.amount",
					: "number",
					: "Amount",
					: () => .. !== "free",
					: { : 1, : 1 },
				},
				{
					: "field",
					: "payment.currency",
					: "select",
					: "Currency",
					: [
						{ : "USD", : "USD" },
						{ : "EUR", : "EUR" },
						{ : "GBP", : "GBP" },
					],
				},
				{
					: "field",
					: "payment.interval",
					: "select",
					: "Recurring interval",
					: () => .. === "recurring",
					: [
						{ : "monthly", : "Monthly" },
						{ : "annual", : "Annual" },
					],
				},
			],
		},
	],
})
 
export function () {
	const [] = (
		() => new ({ : { : { : false } } }),
	)
	return (
		< ={}>
			< />
		</>
	)
}
 
function () {
	const [, ] = <"create" | "edit">("edit")
	const  = ({
		: ["campaign-draft", "campaign-204"],
		: () => (, 390),
	})
	const  = ({
		: ["campaign-segments"],
		: () =>
			(
				[
					{ : "active-members", : "Active members" },
					{ : "new-readers", : "New readers" },
					{ : "past-participants", : "Past participants" },
				],
				470,
			),
	})
	const  = ({
		: (: ) =>
			({ : `campaign-${.. + 700}` }, 430),
	})
	const  = ({
		: (: ) =>
			({ : . ?? "campaign-missing" }, 360),
	})
	const [, ] = ("Loaded an editable campaign draft.")
	async function (
		: ,
		: "create" | "edit",
	) {
		try {
			if ( === "edit") {
				const  = await .()
				(
					`Updated ${.} with ${..} channel(s).`,
				)
				return
			}
 
			const  = await .()
			(
				`Created ${.} with ${..} channel(s).`,
			)
		} catch {
			(
				"The campaign API did not respond. The editor remains available.",
			)
		}
	}
	const :  = { : . ?? [] }
	const  = .(, {
		: ,
		,
		: ({  }) => (, "create"),
	})
	const  = .(, {
		: ,
		,
		: ({  }) => (, "edit"),
	})
 
	if (. || .)
		return (
			< ="form-please-complex">
				Loading campaign builder…
			</>
		)
	if (. || .)
		return (
			< ="form-please-complex">
				Could not load campaign resources.
			</>
		)
	let  = 
	let  = "Create campaign"
	if ( === "edit") {
		 = 
		 = "Update campaign"
	}
	let  = 
	if (. || .) {
		 = "Saving campaign…"
	}
	const  = ..()
	const  = .(..)
		.(([, ]) => )
		.(([]) => )
 
	return (
		<
			="Campaign builder example"
			="form-please-complex"
		>
			< ="form-please-complex__kicker">
				Seven-template campaign builder
			</>
			< ="form-please-complex__summary">
				One shared audience and schedule model drives seven distinct payload
				branches, conditional quantities, payment variants, and create/edit
				mutations.
			</>
			< ="form-please-complex__mode">
				<>Editor mode</>
				<
					={ === "edit"}
					={() => ("edit")}
					="button"
				>
					Edit loaded draft
				</>
				<
					={ === "create"}
					={() => ("create")}
					="button"
				>
					Start new campaign
				</>
			</>
			<.
				="form-please-complex__form"
				={}
				={}
			>
				< ="form-please-complex__actions">
					<. ="form-please-complex__primary">
						{}
					</.>
					< ="polite">{}</>
				</>
			</.>
			<
				="Campaign preview"
				="form-please-complex__preview"
			>
				<>{.}</>
				<>
					{.} · {.(", ") || "no channels"}
				</>
			</>
		</>
	)
}
 
function (: (typeof )[number]): boolean {
	return (
		 === "fundraiser" ||
		 === "course-drop" ||
		 === "product-launch"
	)
}
 
function <>(: , : number): <> {
	return new (() =>
		.(() => (), ),
	)
}

Compare the other workflows in Examples.