Wizard form
An entityEdit screen with EditLayout.mode: "wizard" — one section per
step instead of one long form. The final step is a review section
(kind: "extension") that reads the wizard’s live values instead of
re-fetching them, and draft: true resumes an abandoned wizard from where
the user left off.
What it shows
Section titled “What it shows”mode: "wizard"— the same three sections a"single"layout would render all at once instead render one per step, with a progress indicator and per-step validation. The boot-validator requires at least two sections, each with a non-emptytitle.- A review step as an extension section — the third step
(
kind: "extension") mountsListingReviewSection, a client component resolved by the__componentname at render time. It receives the host form’s current values throughExtensionSectionProps.values(the same live snapshot the other steps edit) and renders them read-only viaDetailList— no second fetch, no duplicated state. draft: true— persists the in-progress wizard as a resumable draft. This flag only does two things in the feature: it requiresmode: "wizard"and it requires the bundledform-draftfeature to be mounted alongside this one (both enforced at boot). The actual save/resume/discard wiring happens automatically insideRenderEdit— this recipe never calls a form-draft handler from its own code.
Feature composition
Section titled “Feature composition”listing — the entity being created through the wizardform-draft — bundled feature backing draft: true (mounted, never called directly by this feature — RenderEdit wires it client-side)config — form-draft requires this for its retention-days setting; mount createConfigFeature() alongside form-draft- A user opens the
listing-wizardscreen and fills in the “Basics” step, then “Pricing”. - If they navigate away mid-wizard,
RenderEdithas already saved a draft keyed by the screen id; reopening the screen resumes those values instead of starting over. - On the “Review” step,
ListingReviewSectionreads the values entered so far straight from the host form and displays them read-only — no extra network round-trip. - Submitting creates the
listingthrough the standard CRUD create and discards the draft.
When to reach for it
Section titled “When to reach for it”A form is long enough that one page of fields overwhelms the user, or
losing everything on an accidental tab close is a real cost — onboarding,
a multi-part application, anything a user is likely to fill in over more
than one sitting. Use a "single"-mode layout instead when the form is
short enough to fit on one screen; wizard mode’s per-step validation and
draft persistence are overhead a short form doesn’t need.
Source
Section titled “Source”The feature entry point is src/feature.ts; the review-step component is
src/web/listing-review-section.tsx. The integration test under
src/__tests__/ covers the entity’s CRUD create through the wizard’s
fields and the form-draft save/get/discard round-trip that backs
draft: true. e2e/wizard.spec.ts is a Playwright spec against the real
client bundle — step navigation, a blocked step on a validation error,
draft-resume after a page reload, and that a successful submit discards
the draft (reopening the wizard starts fresh on step 1).
Source code
Section titled “Source code”The feature entry point — embedded straight from the source file, so the code here is exactly what runs. Multi-file samples keep their remaining files next to it on GitHub (link below):
// Wizard Form Sample// Shows: EditLayout.mode: "wizard" splitting one entityEdit screen into// steps, a review step (kind: "extension") that reads the host form's// live values through ExtensionSectionProps.values instead of its own// fetch, and draft: true resuming an in-progress wizard via the bundled// form-draft feature — the actual save/get/discard wiring is automatic// client-side, this feature only turns the two flags on. form-draft itself// requires the bundled "config" feature (its retention-days setting), so// any app mounting this recipe must mount that too.
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";import { listingEntity } from "./entities/listing";
export { listingEntity } from "./entities/listing";
const editorWrite = { access: { roles: ["Admin", "User"] } } as const;const openRead = { access: { openToAll: true } } as const;
export const listingsFeature = defineFeature("listings", (r) => { r.crud("listing", listingEntity, { write: editorWrite, read: openRead });
r.screen({ id: "listing-wizard", type: "entityEdit", entity: "listing", layout: { mode: "wizard", draft: true, sections: [ { title: "Basics", fields: ["title", "category"] }, { title: "Pricing", fields: ["price", "condition"] }, { kind: "extension", title: "Review", component: { react: { __component: "ListingReviewSection" } }, }, ], }, access: editorWrite.access, });});📄 On GitHub: samples/recipes/wizard-form/src/feature.ts