Skip to content

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.

  • 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-empty title.
  • A review step as an extension section — the third step (kind: "extension") mounts ListingReviewSection, a client component resolved by the __component name at render time. It receives the host form’s current values through ExtensionSectionProps.values (the same live snapshot the other steps edit) and renders them read-only via DetailList — 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 requires mode: "wizard" and it requires the bundled form-draft feature to be mounted alongside this one (both enforced at boot). The actual save/resume/discard wiring happens automatically inside RenderEdit — this recipe never calls a form-draft handler from its own code.
listing — the entity being created through the wizard
form-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
  1. A user opens the listing-wizard screen and fills in the “Basics” step, then “Pricing”.
  2. If they navigate away mid-wizard, RenderEdit has already saved a draft keyed by the screen id; reopening the screen resumes those values instead of starting over.
  3. On the “Review” step, ListingReviewSection reads the values entered so far straight from the host form and displays them read-only — no extra network round-trip.
  4. Submitting creates the listing through the standard CRUD create and discards the draft.

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.

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).


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