Skip to content

Patterns

Common Form Intelligence recipes — wizard, autosave, offline submit, plugins.

Previous: Plugins · Next: Migration

Import path

Recipes below use @jayoncode/form-intelligence unless noted. Formatters → /format; lifecycle plugins → /plugins; schema packages → @jayoncode/form-intelligence-zod (etc.). Entrypoints.


Multi-step wizard

ts
const form = createForm({
  initialValues: { email: "", plan: "", card: "" },
  validators: {
    email: [required, email],
    plan: [required],
    card: [required],
  },
  workflow: {
    wizard: {
      initialStep: 0,
      steps: [
        { id: "account", fields: ["email"] },
        { id: "plan", fields: ["plan"] },
        { id: "payment", fields: ["card"] },
      ],
    },
  },
  onSubmit: async (values) => api.checkout(values),
});

await form.workflow.next(); // validates current step fields
form.workflow.prev();
await form.workflow.goTo(2);

form.state.workflow.currentStep;
form.state.workflow.progress; // 0–100

Autosave + draft restore

ts
createForm({
  initialValues: { body: "" },
  workflow: {
    autosave: {
      enabled: true,
      debounceMs: 400,
      onSave: (values) => api.patchDraft(values),
    },
    draft: {
      enabled: true,
      storageKey: "editor:draft",
    },
  },
});

Status: form.state.workflow.isAutosaving, form.state.workflow.lastAutosaveAt.

Manual save: form.saveDraft().


Offline submit queue

ts
createForm({
  initialValues: { email: "" },
  workflow: {
    offlineQueue: {
      enabled: true,
      storageKey: "signup:offline",
    },
  },
  onSubmit: async (values) => api.register(values),
});

// When back online:
await form.flushOfflineQueue();

Pair with createBrowserLifecyclePlugin({ flushOfflineQueueOnOnline: true }).


Plugin hooks

ts
createForm({
  initialValues: { email: "" },
  plugins: [
    {
      name: "audit",
      setup(_form, api) {
        api.on("beforeSubmit", () => {
          if (!window.confirm("Submit?")) return false;
        });
        api.on("afterValidate", ({ valid }) => {
          console.log("valid?", valid);
        });
        return {
          onDestroy() {
            console.log("cleanup");
          },
        };
      },
    },
  ],
});
// Or later: form.use({ name: "audit", setup(...) { ... } });

Hooks: beforeValidate, afterValidate, beforeSubmit, afterSubmit, onAutosave, onDraftRestore. Full authoring rules (ownership, engines, testing): Plugins — author guide.


Schema adapters (Zod)

ts
import { zodAdapter } from "@jayoncode/form-intelligence-zod";
import { z } from "zod";

createForm({
  initialValues: { email: "" },
  schema: zodAdapter(z.object({ email: z.string().email() })),
  onSubmit,
});

Core stays free of Zod — adapters implement SchemaAdapter.


Headless HTML progressive enhancement

ts
createForm({
  target: "#register",
  // schema optional — HTML required / type="email" / minlength also import as validators
  schema: { email: "email", password: "password" },
  onSubmit: async (values) => api.register(values),
});
html
<form id="register">
  <input name="email" required type="email" />
  <input name="password" required minlength="8" type="password" />
  <button type="submit">Register</button>
</form>

Phase 1 attributes become FI validators on attach (Field > Schema > HTML). Errors announce with role="alert" and aria-invalid automatically (novalidate). Lab: HTML constraints.

Done? Browse the API Reference or open the playground.

JOC — headless @jayoncode/* TypeScript libraries. Docs sync from package source via TypeDoc and sync scripts.