Makerspace launch wizard
The active screen is external React state. The form definition reads it through context, while React Hook Form stores only editable makerspace input.
Try the complete flow
Move through all four stages. On the location stage, edit the postal code and apply the asynchronous address result. Add or reorder capacity and gallery rows. The final submit validates the complete schema and performs three fake writes.
What this form demonstrates
- Application-owned navigation passed through form context.
- Context-provided campus and region options.
- Address lookup driven by
useWatch. - Typed coordinate updates from an application-owned control.
- Native file input and generated arrays.
- One transformed output used by three sequential mutations.
"Form, Please" does not include a wizard engine. This example composes the workflow from external screen state and conditional sections. Put a stage in the schema only when the server stores it as a domain field, such as an approval stage. Do not put the current wizard screen in the schema only to drive UI.
Use the Product workflows tutorial as the canonical navigation, validation, progress, review, and first-invalid pattern.
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 "react-hook-form"
import { } from "zod"
const =
.({
: .({
: .().(4, "Use a distinctive public name"),
: .().(1, "Choose a campus"),
: .().(60, "Write at least 60 characters"),
}),
: .({
: .().(1, "Choose a region"),
: .().(3, "Enter a postal code"),
: .().(6, "Enter a complete address"),
: .().(-90).(90),
: .().(-180).(180),
}),
: .({
:
.<File | undefined>(
() =>
=== ||
(typeof !== "undefined" && instanceof ),
"Choose an image file",
)
.(),
: .(
.({
: .().("Enter a valid media URL"),
: .().(2, "Add a caption"),
}),
),
}),
:
.(
.({
: .().(2, "Name the capacity band"),
: .().().(1).(500),
: .().(0),
}),
)
.(1, "Add at least one capacity band"),
: .({
: .(),
: .(),
: .(),
: .(),
}),
:
.()
.(20, "Explain how members enter the space"),
: .({
: .({
: .(),
: .().(1).(90).(),
}),
: .({
: .(),
: .().(1).(90).(),
}),
: .({
: .(),
: .().(1).(90).(),
}),
: .({
: .(),
: .().(1).(90).(),
}),
}),
})
.((, ) => {
for (const [, ] of .(.)) {
if (. && . === ) {
.({
: "custom",
: ["promotions", , "percent"],
: "Set the active reduction",
})
}
}
const = [....].(
(, ) => . - .,
)
for (let = 1; < .; += 1) {
const = [ - 1]
const = []
if (
!== &&
!== &&
. < .
) {
.({
: "custom",
: ["capacityBands"],
: "Larger capacity bands cannot cost less than smaller bands",
})
break
}
}
})
.(() => ({
...,
: .(.).(
() => .,
).,
}))
type = <typeof >
type = <typeof >
const = ["identity", "location", "capacity", "publishing"] as
type = (typeof )[number]
type = {
readonly :
readonly : readonly {
readonly : string
readonly : string
}[]
readonly : readonly {
readonly : string
readonly : string
}[]
}
const = {
: {
: "Copperline Commons",
: "river-yard",
:
"A shared fabrication floor for neighborhood prototypes, repair circles, material experiments, and open skill exchanges.",
},
: {
: "north-bank",
: "N4 7PX",
: "48 Foundry Lane",
: 51.542,
: -0.102,
},
: {
: ,
: [
{
: "https://example.test/media/workbench.jpg",
: "Shared assembly workbench",
},
],
},
: [
{ : "Bench session", : 8, : 45 },
{ : "Open floor", : 24, : 110 },
],
: {
: true,
: true,
: true,
: false,
},
:
"Use the east courtyard entrance and check in at the tool desk before entering the floor.",
: {
: { : true, : 20 },
: { : false, : },
: { : true, : 15 },
: { : false, : },
},
} satisfies
const = ({
: (),
: (),
})
const = .<>()
const = {
: "Identity",
: "Location",
: "Capacity & media",
: "Publishing",
} satisfies <(typeof )[number], string>
const = {
: "Identity",
: "Location",
: "Capacity & media",
: "Publishing",
} satisfies <(typeof )[number], string>
type = <typeof , >
function ({
,
,
}: {
readonly :
readonly : (: ) => void
}) {
const = .()
const = [ + 1] ??
let = (
<. ="form-please-complex__primary">
Publish makerspace
</.>
)
if ( < . - 1) {
= (
<
="form-please-complex__primary"
={() => ()}
="button"
>
Continue to {[]}
</>
)
}
return (
< ="Launch stages" ="form-please-complex__wizard">
<>
{.(() => {
let : "step" | undefined
let : string | undefined
if ( === ) {
= "step"
= "var(--fp-docs-rust)"
}
return (
< ={} ={}>
<
={{ }}
={() => ()}
="button"
>
{[]}
</>
</>
)
})}
</>
< ="form-please-complex__actions">
<
={ === 0}
={() => ([ - 1] ?? )}
="button"
>
Back
</>
{}
</>
</>
)
}
function ({
,
,
,
}: {
readonly :
readonly :
readonly : (: ) => void
}) {
const = ({ : .. }) as
let = null
if ( === "location") {
= (
<>
< ={} ={..} />
<
="Coordinate preview"
="form-please-complex__preview"
>
<>{..}</>
<>
{...(3)},{" "}
{...(3)} ·{" "}
{..}
</>
</>
</>
)
}
return (
<>
< ={} ={} />
{}
</>
)
}
function ({
,
,
}: {
readonly :
readonly : string
}) {
const = ({
: ["address-lookup", ],
: () => {
let = "12 Workshop Crescent"
if (.().("N")) {
= "48 Foundry Lane"
}
return (
{
,
: 51.542,
: -0.102,
},
320,
)
},
: .(). >= 3,
})
let = "Address suggestion ready"
if (.) = "Resolving postal code…"
return (
< ="form-please-complex__embedded">
<>{}</>
<
={. === }
={() => {
if (. === ) return
..("location.address", ..)
..("location.latitude", ..)
..("location.longitude", ..)
}}
="button"
>
Apply resolved address
</>
</>
)
}
const = .(, {
: [
{
: "section",
: "identity",
: "Makerspace identity",
: (, { }) => . === "identity",
: 2,
: [
{
: "field",
: "identity.name",
: "text",
: "Public name",
: true,
},
{
: "field",
: "identity.campusId",
: "select",
: "Campus",
: ({ }) => .,
},
{
: "field",
: "identity.description",
: "textarea",
: "Public description",
: "Explain the work this place makes possible.",
: "full",
: { : 5 },
},
],
},
{
: "section",
: "location",
: "Location",
: (, { }) => . === "location",
: 2,
: [
{
: "field",
: "location.regionId",
: "select",
: "Region",
: ({ }) => .,
},
{
: "field",
: "location.postalCode",
: "text",
: "Postal code",
},
{
: "field",
: "location.address",
: "text",
: "Street address",
: "full",
},
{
: "field",
: "location.latitude",
: "number",
: "Latitude",
: { : -90, : 90, : 0.001 },
},
{
: "field",
: "location.longitude",
: "number",
: "Longitude",
: { : -180, : 180, : 0.001 },
},
],
},
{
: "section",
: "capacity",
: "Capacity, media, and amenities",
: (, { }) => . === "capacity",
: [
{
: "array",
: "capacityBands",
: "Capacity bands",
: { : "", : 1, : 0 },
: [
{
: "field",
: "label",
: "text",
: "Band name",
},
{
: "field",
: "people",
: "number",
: "People",
: { : 1, : 500, : 1 },
},
{
: "field",
: "hourlyRate",
: "number",
: "Hourly rate",
: { : 0, : 5 },
},
],
},
{
: "field",
: "media.cover",
: "file",
: "Cover image",
: { : "image/*" },
},
{
: "array",
: "media.gallery",
: "Gallery",
: "Reorder references without losing row state.",
: { : "", : "" },
: [
{
: "field",
: "assetUrl",
: "text",
: "Media URL",
},
{
: "field",
: "caption",
: "text",
: "Caption",
},
],
},
{
: "section",
: "amenities",
: "Amenities",
: 2,
: [
{
: "field",
: "amenities.stepFree",
: "checkbox",
: "Step-free",
},
{
: "field",
: "amenities.ventilation",
: "checkbox",
: "Extract ventilation",
},
{
: "field",
: "amenities.toolLibrary",
: "checkbox",
: "Tool library",
},
{
: "field",
: "amenities.quietZone",
: "checkbox",
: "Quiet zone",
},
],
},
],
},
{
: "section",
: "publishing",
: "Publishing rules",
: (, { }) => . === "publishing",
: [
{
: "field",
: "accessInstructions",
: "textarea",
: "Access instructions",
: { : 4 },
},
("launch", "Launch offer"),
("student", "Student access"),
("community", "Community partner"),
("offPeak", "Off-peak hours"),
],
},
],
})
function (
: "launch" | "student" | "community" | "offPeak",
: string,
) {
return {
: "section" as ,
: `promotion-${}`,
,
: 2 as ,
: [
{
: "field" as ,
: `promotions.${}.enabled` as ,
: "checkbox" as ,
: "Enabled",
},
{
: "field" as ,
: `promotions.${}.percent` as ,
: "number" as ,
: "Reduction percent",
: ({
[`promotions.${}.enabled`]: ,
}: <string, unknown>) => (),
: { : 1, : 90, : 1 },
},
],
}
}
export function () {
const [] = (
() => new ({ : { : { : false } } }),
)
return (
< ={}>
< />
</>
)
}
function () {
const [, ] = <>("identity")
const = ({
: ["maker-campuses"],
: () =>
(
[
{ : "river-yard", : "River Yard" },
{ : "civic-annex", : "Civic Annex" },
],
330,
),
})
const = ({
: ["maker-regions"],
: () =>
(
[
{ : "north-bank", : "North Bank" },
{ : "old-market", : "Old Market" },
],
470,
),
})
const = ({
: (: ) =>
({ : ... + 900 }, 360),
})
const = ({
: (: ) =>
({ : ... }, 340),
})
const = ({
: (: ) =>
({ : . }, 420),
})
const [, ] = ("Complete the four stages to publish.")
const = (
() => ({
,
: . ?? [],
: . ?? [],
}),
[, ., .],
)
const = .(, {
,
,
async ({ }) {
try {
const = await .()
const = await .()
const = await .()
(
`Space ${.} published with ${.} gallery item(s) and ${.} offer(s).`,
)
} catch {
("Publishing paused. The wizard kept every value.")
}
},
})
if (. || .) {
return (
< ="form-please-complex">
Loading launch references…
</>
)
}
if (. || .) {
return (
< ="form-please-complex">
Could not open the launch wizard.
</>
)
}
let =
if (. || . || .) {
= "Saving location, media, and release…"
}
return (
<
="Makerspace launch wizard example"
="form-please-complex"
>
< ="form-please-complex__kicker">Four-stage launch wizard</>
< ="form-please-complex__summary">
An external screen controls conditional sections through form context.
Domain input still keeps address lookup, media rows, pricing bands, four
offers, and three writes in one form.
</>
<. ="form-please-complex__form" ={}>
< ={} ={} ={} />
<. />
</.>
< ="form-please-complex__network" ="polite">
{}
</>
</>
)
}
function <>(: , : number): <> {
return new (() =>
.(() => (), ),
)
}Compare the other workflows in Examples.