--- name: formisch-usage description: Form handling with Formisch, the type-safe form library for modern frameworks. Use when the user needs to create forms, handle form state, validate form inputs, or work with Formisch. license: MIT metadata: author: open-circle version: "1.0" --- # Formisch Usage This skill helps AI agents work effectively with [Formisch](https://formisch.dev/), the schema-based, headless form library for modern frameworks. ## When to Use This Skill - When the user asks about form handling with Formisch - When managing form state and validation - When working with React, Vue, Solid, Preact, Svelte, or Qwik forms - When integrating Valibot schemas with forms ## Introduction Formisch is a schema-based, headless form library that works across multiple frameworks. Key highlights: - **Small bundle size** — Starting at ~2.5 kB - **Schema-based validation** — Uses Valibot for type-safe validation - **Headless design** — You control the UI completely - **Type safety** — Full TypeScript support with autocompletion - **Framework-native** — Native performance for each supported framework ### Supported Frameworks | Framework | Package | Hook/Primitive | | --------- | ------------------ | -------------- | | React | `@formisch/react` | `useForm` | | Vue | `@formisch/vue` | `useForm` | | SolidJS | `@formisch/solid` | `createForm` | | Preact | `@formisch/preact` | `useForm` | | Svelte | `@formisch/svelte` | `createForm` | | Qwik | `@formisch/qwik` | `useForm$` | ## Installation ### 1. Install Valibot (peer dependency) ```bash npm install valibot ``` ### 2. Install Formisch for your framework ```bash npm install @formisch/react # React npm install @formisch/vue # Vue npm install @formisch/solid # SolidJS npm install @formisch/preact # Preact npm install @formisch/svelte # Svelte npm install @formisch/qwik # Qwik ``` ## Core Concepts ### Schema-First Design Every form starts with a Valibot schema. Types are automatically inferred from the schema. ```ts import * as v from "valibot"; const LoginSchema = v.object({ email: v.pipe( v.string("Please enter your email."), v.nonEmpty("Please enter your email."), v.email("The email address is badly formatted."), ), password: v.pipe( v.string("Please enter your password."), v.nonEmpty("Please enter your password."), v.minLength(8, "Your password must have 8 characters or more."), ), }); ``` ### Form Store The form store manages all form state. Access it via the framework-specific hook/primitive. **Form Store Properties:** - `isSubmitting` — Form is currently being submitted - `isSubmitted` — Form has been successfully submitted - `isValidating` — Validation is in progress - `isTouched` — At least one field has been touched - `isDirty` — At least one field differs from initial value - `isValid` — All fields pass validation - `errors` — Root-level validation errors ### Field Store Each field has its own reactive store with: - `path` — Path array to the field - `input` — Current field value - `errors` — Field-specific errors - `isTouched` — Field has been focused and blurred - `isDirty` — Field value differs from initial value - `isValid` — Field passes validation - `props` — Props to spread onto input elements ### Dirty Tracking Formisch tracks two inputs per field: - **Initial input** — Baseline for dirty tracking (server state) - **Current input** — What the user is editing (client state) `isDirty` becomes `true` when current input differs from initial input. ## Framework Examples ### React Example ```tsx import { Field, Form, useForm } from "@formisch/react"; import type { SubmitHandler } from "@formisch/react"; import * as v from "valibot"; const LoginSchema = v.object({ email: v.pipe(v.string(), v.email()), password: v.pipe(v.string(), v.minLength(8)), }); export default function LoginPage() { const loginForm = useForm({ schema: LoginSchema, }); const handleSubmit: SubmitHandler = (output) => { console.log(output); // { email: string, password: string } }; return (
{(field) => (
{field.errors &&
{field.errors[0]}
}
)}
{(field) => (
{field.errors &&
{field.errors[0]}
}
)}
); } ``` ### Vue Example ```vue ``` ### SolidJS Example ```tsx import { Field, Form, createForm } from "@formisch/solid"; import type { SubmitHandler } from "@formisch/solid"; import * as v from "valibot"; const LoginSchema = v.object({ email: v.pipe(v.string(), v.email()), password: v.pipe(v.string(), v.minLength(8)), }); export default function LoginPage() { const loginForm = createForm({ schema: LoginSchema, }); const handleSubmit: SubmitHandler = (output) => { console.log(output); }; return (
{(field) => (
{field.errors &&
{field.errors[0]}
}
)}
{(field) => (
{field.errors &&
{field.errors[0]}
}
)}
); } ``` ### Svelte Example ```svelte
{#snippet children(field)}
{#if field.errors}
{field.errors[0]}
{/if}
{/snippet}
{#snippet children(field)}
{#if field.errors}
{field.errors[0]}
{/if}
{/snippet}
``` ### Qwik Example ```tsx import { Field, Form, useForm$ } from "@formisch/qwik"; import { component$ } from "@qwik.dev/core"; import * as v from "valibot"; const LoginSchema = v.object({ email: v.pipe(v.string(), v.email()), password: v.pipe(v.string(), v.minLength(8)), }); export default component$(() => { const loginForm = useForm$({ schema: LoginSchema, }); return (
console.log(output)}> (
{field.errors.value &&
{field.errors.value[0]}
}
)} /> (
{field.errors.value &&
{field.errors.value[0]}
}
)} /> ); }); ``` ## Form Configuration ```ts const form = useForm({ // Required: Valibot schema schema: MySchema, // Optional: Initial values (partial allowed) initialInput: { email: "user@example.com", }, // Optional: When first validation occurs // Options: 'initial' | 'blur' | 'input' | 'submit' (default) validate: "submit", // Optional: When revalidation occurs after first validation // Options: 'blur' | 'input' (default) | 'submit' revalidate: "input", }); ``` ## Field Paths Paths are type-safe arrays that reference fields in your schema. ```tsx // Top-level field // Nested field (schema: { user: { email: string } }) // Array item field (schema: { todos: [{ label: string }] }) // Dynamic array index {items.map((item, index) => ( ))} ``` ## Form Methods All methods follow a consistent API pattern: - **First parameter**: Form store - **Second parameter**: Config object ### Reading Values ```ts import { getInput, getErrors, getAllErrors } from "@formisch/react"; // Get field value const email = getInput(form, { path: ["email"] }); // Get entire form input const allInputs = getInput(form); // Get field errors const emailErrors = getErrors(form, { path: ["email"] }); // Get all errors across all fields const allErrors = getAllErrors(form); ``` ### Setting Values ```ts import { setInput, setErrors, reset } from "@formisch/react"; // Set field value (updates current input, not initial) setInput(form, { path: ["email"], input: "new@example.com" }); // Set field errors manually setErrors(form, { path: ["email"], errors: ["Email already taken"] }); // Clear errors setErrors(form, { path: ["email"], errors: null }); // Reset entire form reset(form); // Reset with new initial values reset(form, { initialInput: { email: "", password: "" }, }); // Reset but keep current input reset(form, { initialInput: newServerData, keepInput: true, }); ``` ### Form Control ```ts import { validate, focus, submit, handleSubmit } from "@formisch/react"; // Validate form manually const isValid = await validate(form); // Validate and focus first error field await validate(form, { shouldFocus: true }); // Focus a specific field focus(form, { path: ["email"] }); // Programmatically submit form submit(form); // Create submit handler for external buttons const onExternalSubmit = handleSubmit(form, (output) => { console.log(output); }); ``` ## Field Arrays For dynamic lists of fields, use `FieldArray` with array manipulation methods. ### Schema ```ts const TodoSchema = v.object({ heading: v.pipe(v.string(), v.nonEmpty()), todos: v.pipe( v.array( v.object({ label: v.pipe(v.string(), v.nonEmpty()), deadline: v.pipe(v.string(), v.nonEmpty()), }), ), v.nonEmpty(), v.maxLength(10), ), }); ``` ### React Example ```tsx import { Field, FieldArray, Form, useForm, insert, remove, move, swap, } from "@formisch/react"; export default function TodoPage() { const todoForm = useForm({ schema: TodoSchema, initialInput: { heading: "", todos: [{ label: "", deadline: "" }], }, }); return (
console.log(output)}> {(field) => } {(fieldArray) => (
{fieldArray.items.map((item, index) => (
{(field) => ( )} {(field) => ( )}
))} {fieldArray.errors &&
{fieldArray.errors[0]}
}
)}
); } ``` ### Array Methods ```ts import { insert, remove, move, swap, replace } from "@formisch/react"; // Add item at end insert(form, { path: ["todos"], initialInput: { label: "", deadline: "" } }); // Add item at specific index insert(form, { path: ["todos"], at: 0, initialInput: { label: "", deadline: "" }, }); // Remove item at index remove(form, { path: ["todos"], at: index }); // Move item from one index to another move(form, { path: ["todos"], from: 0, to: 3 }); // Swap two items swap(form, { path: ["todos"], at: 0, and: 1 }); // Replace item at index replace(form, { path: ["todos"], at: 0, initialInput: { label: "New task", deadline: "2024-12-31" }, }); ``` ## TypeScript Integration ### Type Inference Types are automatically inferred from your Valibot schema: ```ts const LoginSchema = v.object({ email: v.pipe(v.string(), v.email()), password: v.pipe(v.string(), v.minLength(8)), }); const form = useForm({ schema: LoginSchema }); // form is FormStore // Submit handler receives typed output const handleSubmit: SubmitHandler = (output) => { output.email; // ✓ string output.password; // ✓ string output.username; // ✗ TypeScript error }; ``` ### Input vs Output Types Schemas with transformations have different input and output types: ```ts const ProfileSchema = v.object({ age: v.pipe( v.string(), // Input: string v.transform((input) => Number(input)), // Output: number v.number(), ), birthDate: v.pipe( v.string(), // Input: string v.transform((input) => new Date(input)), // Output: Date v.date(), ), }); // In Field: field.input is string // In onSubmit: output.age is number, output.birthDate is Date ``` ### Type-Safe Props Pass forms to child components with proper typing: ```tsx import type { FormStore } from "@formisch/react"; type FormContentProps = { of: FormStore; }; function FormContent({ of }: FormContentProps) { return (
console.log(output)}> {/* ... */}
); } ``` ### Generic Field Components Create reusable field components with proper typing: ```tsx import { useField, type FormStore } from "@formisch/react"; import * as v from "valibot"; type EmailInputProps = { of: FormStore>; }; function EmailInput({ of }: EmailInputProps) { const field = useField(of, { path: ["email"] }); return (
{field.errors &&
{field.errors[0]}
}
); } ``` ### Available Types ```ts import type { FormStore, // Form store type FieldStore, // Field store type FieldArrayStore, // Field array store type SubmitHandler, // Submit handler function type ValidPath, // Valid field path type ValidArrayPath, // Valid array field path type Schema, // Base schema type from Valibot } from "@formisch/react"; ``` ## Validation Timing ### validate Option Controls when the **first** validation occurs: | Value | Description | | ----------- | ------------------------------------------ | | `'initial'` | Validate immediately on form creation | | `'blur'` | Validate when field loses focus | | `'input'` | Validate on every input change | | `'submit'` | Validate only on form submission (default) | ### revalidate Option Controls when validation runs **after** the first validation: | Value | Description | | ---------- | ------------------------------------------ | | `'blur'` | Revalidate when field loses focus | | `'input'` | Revalidate on every input change (default) | | `'submit'` | Revalidate only on form submission | ## Special Inputs ### Select (Single) ```tsx {(field) => ( )} ``` ### Select (Multiple) ```tsx {(field) => ( )} ``` ### Checkbox ```tsx {(field) => } ``` ### File Input File inputs cannot be controlled. Handle via UI around them: ```tsx {(field) => (
{field.input && {field.input.name}}
)}
``` ## useField Hook For complex field components, use the `useField` hook instead of the `Field` component: ```tsx import { useField } from "@formisch/react"; function EmailInput({ form }) { const field = useField(form, { path: ["email"] }); // Access field state in component logic useEffect(() => { if (field.errors) { console.log("Email has errors:", field.errors); } }, [field.errors]); return (
{field.errors &&
{field.errors[0]}
}
); } ``` **When to use which:** - **`Field` component** — Multiple fields in the same component - **`useField` hook** — Single field with component logic access ## Async Submission ```tsx const handleSubmit: SubmitHandler = async (values) => { try { const response = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(values), }); if (!response.ok) { // Set server-side errors const data = await response.json(); setErrors(form, { path: ["email"], errors: [data.error] }); } } catch (error) { console.error("Submission failed:", error); } }; ``` ## Common Patterns ### Loading State ```tsx ``` ### Submit on Enter Formisch handles this automatically via the native `
` element. ### Reset After Success ```tsx const handleSubmit: SubmitHandler = async (values) => { await saveData(values); // Full reset to initial state reset(form); // Or reset but keep current input values reset(form, { keepInput: true }); }; ``` ### Server Data Sync When server data changes, update the baseline without losing user edits: ```tsx // After refetching data from server reset(form, { initialInput: newServerData, keepInput: true, // Keep user's current edits keepTouched: true, // Keep touched state (optional) }); ``` ### Conditional Fields ```tsx {(field) => } ; { getInput(form, { path: ["hasAccount"] }) && ( {(field) => } ); } ``` ## Additional Resources - [Formisch Documentation](https://formisch.dev/) - [Formisch GitHub](https://github.com/open-circle/formisch) - [Valibot Documentation](https://valibot.dev/)