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.
What this form demonstrates
- Conditional sections for seven schema branches.
- Context-provided audience options.
- Separate create and update mutations outside form state.
useWatchfor 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.