Skip to content
Ilha
Esc
navigateopen⌘Jpreview
On this page

Forms

type-safe, schema-validated forms for ilha with the @ilha/store/form helpers.

The @ilha/store/form import path adds small helpers on Standard Schema (Zod, Valibot, ArkType, and others) for extracting, validating, and mapping form data.

A canonical form pattern: keep validation results in a small store, and gate submissions with validateWithSchema.

Helpers

import {
  extractFormData,
  validateWithSchema,
  validateWithSchemaAsync,
  issuesToErrors,
} from "@ilha/store/form";

extractFormData(source)

Turns an HTMLFormElement (or FormData) into a plain object. Single fields stay scalar; repeated keys collapse to arrays. File inputs pass through as File values.

const data = extractFormData(event.target as HTMLFormElement);
// → { email: "ada@example.com", role: ["admin", "editor"] }

validateWithSchema(schema, data)

Runs a Standard Schema synchronously. Never throws. Returns { ok: true, data } or { ok: false, issues }. Use validateWithSchemaAsync for async refinements.

issuesToErrors(issues)

Flattens Standard Schema issues into Record<string, string[]> keyed by dot-separated path. Form-level errors (no path) land under "".

issuesToErrors([
  { message: "Required", path: ["email"] },
  { message: "Invalid", path: ["user", "email"] },
]);
// → { email: ["Required"], "user.email": ["Invalid"] }

Full example — contact form

import { store } from "@ilha/store";
import {
  extractFormData,
  validateWithSchema,
  issuesToErrors,
} from "@ilha/store/form";
import type { FormErrors } from "@ilha/store/form";
import { ilha } from "ilha";
import { z } from "zod";

const ContactSchema = z.object({
  name: z.string().min(1, "Name is required"),
  email: z.email("Invalid email"),
  message: z.string().min(10, "Too short"),
});

const formStore = store({ errors: {} as FormErrors })
  .action("submit", (event: SubmitEvent) => {
    const result = validateWithSchema(
      ContactSchema,
      extractFormData(event.target as HTMLFormElement),
    );
    return {
      errors: result.ok ? {} : issuesToErrors(result.issues),
    };
  })
  .build();

const errors = formStore.errors; // state accessor — reactive

export default ilha
  .action("submit", ({ event }) => formStore.submit(event))
  .render(() => (
    <form>
      <input name="name" />
      {errors().name ? (
        <p role="alert">{errors().name[0]}</p>
      ) : null}
      <input name="email" type="email" />
      {errors().email ? (
        <p role="alert">{errors().email[0]}</p>
      ) : null}
      <button type="submit">Send</button>
    </form>
  ));
Topic Guide
validateWithSchema on the store Subscriptions and validation
Bind two-way inputs Bindings
Shared state for the form Store overview

Was this page helpful?