# El Form — Full Reference for LLMs El Form is a TypeScript-first, schema-agnostic React form library. Two APIs cover the full range from "generate a form for me" to "give me total control": - **AutoForm** — render a complete, validated, styled form straight from a schema. - **useForm** — a React Hook Form–compatible hook for fully custom forms. Validation is pluggable: Zod (v3 or v4), Yup, Valibot, a custom function, or none. This document is self-contained: an agent can read it and use El Form correctly without fetching anything else. -------------------------------------------------------------------------------- ## Installation ```bash # Everything (hooks + components + styles) — recommended npm install el-form-react # Just the hooks (custom forms) npm install el-form-react-hooks # Just AutoForm + components npm install el-form-react-components # Framework-agnostic validation engine only npm install el-form-core ``` Validation libraries are optional — install only what you use: ```bash npm install zod # recommended (v3 and v4 both supported) npm install yup npm install valibot ``` Peer requirement: React 18+ (the hooks API works with React 16.8+). -------------------------------------------------------------------------------- ## Packages | Package | Exports | Use when | | -------------------------- | ---------------------------------------- | -------- | | `el-form-react` | re-exports hooks + components + styles | most apps | | `el-form-react-hooks` | `useForm`, `FormProvider`, `useFormContext`, `useFormSelector`, `useField`, `useWatch`, `useFieldArray`, `shallowEqual` | custom forms | | `el-form-react-components` | `AutoForm`, field components, `styles.css` | schema-driven forms | | `el-form-core` | validation engine + utilities | other frameworks / advanced | -------------------------------------------------------------------------------- ## AutoForm — zero boilerplate ```tsx import { AutoForm } from "el-form-react-components"; import "el-form-react-components/styles.css"; // optional zero-config styling import { z } from "zod"; const schema = z.object({ name: z.string().min(1, "Name is required"), email: z.string().email("Invalid email"), message: z.string().min(10, "Message too short"), }); export function ContactForm() { return ( console.log("valid:", data)} onError={(errors) => console.log("invalid:", errors)} /> ); } ``` ### Customizing fields, layout, and async validation ```tsx ``` Common field config keys: `name`, `type` ("text" | "email" | "password" | "number" | "textarea" | "select" | ...), `label`, `placeholder`, `colSpan`, `required`, `min`, `max`, `rows`, `options` (for selects). ### Custom error component ```tsx import type { AutoFormErrorProps } from "el-form-react-components"; const ErrorList: React.FC = ({ errors, touched }) => { const entries = Object.entries(errors).filter(([f]) => touched[f]); if (entries.length === 0) return null; return (
    {entries.map(([field, error]) => (
  • {field}: {error}
  • ))}
); }; ; ``` -------------------------------------------------------------------------------- ## useForm — full control ```tsx import { useForm } from "el-form-react-hooks"; import { z } from "zod"; const schema = z.object({ email: z.string().email(), password: z.string().min(8), }); export function LoginForm() { const { register, handleSubmit, formState } = useForm({ validators: { onChange: schema }, defaultValues: { email: "", password: "" }, }); return (
console.log(data))}> {formState.errors.email && {formState.errors.email}} {formState.errors.password && {formState.errors.password}}
); } ``` `useForm` returns (most-used): `register`, `handleSubmit`, `formState` (`values`, `errors`, `touched`, `isDirty`, `isSubmitting`, `isValid`, `isValidating`, `isSubmitted`, `isSubmitSuccessful`, `submitCount`, `dirtyFields`), `watch`, `setError`, `clearErrors`, `reset`, `setValue`. Each `errors[field]` is a plain string (no `.message`). `isSubmitted`/`isSubmitSuccessful`/`submitCount` track submit attempts and are reset by `reset()`. `isValidating` is `true` while any async validation (onChange/onBlur/submit/`trigger()` async validator) is in flight, else `false`; reset by `reset()`. `dirtyFields` is a reactive, flat, path-keyed map of dirty fields (e.g. `{ "profile.name": true }`) — the reactive twin of `getDirtyFields()` (keys are path strings, not `keyof T`); reset by `reset()`. There is no `defaultValues` or `isLoading` on `formState` — per-field dirty/touched maps are also available as the `getDirtyFields()` / `getTouchedFields()` methods. Validation triggers are configured per event: ```tsx useForm({ validators: { onChange: schema } }); // validate on change useForm({ validators: { onBlur: schema } }); // validate on blur useForm({ validators: { onSubmit: schema } }); // validate on submit ``` Validation timing options (RHF parity): ```tsx // mode: when a field is first validated useForm({ mode: "onChange" }); // validate every change useForm({ mode: "onBlur" }); // validate on blur useForm({ mode: "onTouched" }); // validate on first blur, then every change once touched useForm({ mode: "onSubmit" }); // validate only on submit (default) useForm({ mode: "all" }); // validate on change and blur // reValidateMode (opt-in; default undefined = unchanged): after the form has been // submitted once, pin post-submit re-validation to a single event. useForm({ validators: { onChange: schema }, reValidateMode: "onBlur" }); // Nuance: register's onChange eagerly CLEARS a field's error before the // reValidateMode gate, so with reValidateMode: "onBlur" a post-submit keystroke // visibly clears the error immediately and a later blur re-adds it if still invalid. ``` -------------------------------------------------------------------------------- ## Validation approaches ```tsx // Zod (recommended) const form = useForm({ validators: { onChange: zodSchema } }); // Yup import * as yup from "yup"; const yupSchema = yup.object({ name: yup.string().required() }); const form = useForm({ validators: { onChange: yupSchema } }); // Valibot import * as v from "valibot"; const valibotSchema = v.object({ name: v.pipe(v.string(), v.minLength(1)) }); const form = useForm({ validators: { onChange: valibotSchema } }); // Custom function — return a message string (or undefined when valid) const form = useForm({ validators: { onChange: ({ values }) => values.email?.includes("@") ? undefined : "Invalid email", }, }); // No validation — just state management const form = useForm({ defaultValues: { email: "" } }); ``` -------------------------------------------------------------------------------- ## Error handling ```tsx const { setError, clearErrors, formState } = useForm(); setError("email", "This email is already taken"); // field error setError("general", "Something went wrong"); // form-level error clearErrors("email"); // clear one clearErrors(); // clear all // API errors on submit const onSubmit = async (data) => { try { await submit(data); } catch (err) { if (err.fieldErrors) { Object.entries(err.fieldErrors).forEach(([f, m]) => setError(f, m as string)); } else { setError("general", "Submission failed. Please try again."); } } }; ``` -------------------------------------------------------------------------------- ## Reusable field components ```tsx import { FormProvider, useFormContext } from "el-form-react-hooks"; function Field({ name, label }) { const { register, formState } = useFormContext(); return ( ); } function App() { const form = useForm({ defaultValues: { email: "" } }); return (
); } ``` Three reusability patterns are supported: **context** (`FormProvider` + `useFormContext`), **prop-passing** (``), and **hybrid** (`form || useFormContext()`). -------------------------------------------------------------------------------- ## Reactive hooks (inside ``) `useWatch` — reactively subscribe to value(s) by path; a reactive mirror of `form.watch()` built on the selector store, so each watcher re-renders in isolation. Returns **values only** (use `useField` for `value + error + touched`, or `useFormSelector` for an arbitrary derived slice). Must be used within a ``. ```tsx import { useWatch } from "el-form-react-hooks"; const all = useWatch(); // Partial const email = useWatch("email"); // string const pair = useWatch(["a", "b"]); // { a, b } ``` `useFieldArray` — dynamic array fields. Returns a `fields` array where each row has a stable `id` (use as the React `key`) plus `append`, `prepend`, `insert`, `remove`, `move`, `swap`, `update`, and `replace`. `name` is restricted to array-valued paths; item types are inferred. Works in `FormProvider` (re-renders only when its array changes) or with a `form` prop. ```tsx import { useFieldArray } from "el-form-react-hooks"; const { fields, append, remove } = useFieldArray({ name: "items" }); ``` -------------------------------------------------------------------------------- ## Reactive external values (props / server data) `useForm({ values })` re-syncs the form whenever the `values` object's content changes (deep-compared, so a new-object/same-content render is a no-op — no memoization needed). It takes precedence over `defaultValues` for the initial state. Pair with `keepDirtyValues: true` to keep fields the user is mid-editing while untouched fields sync. This is the el-form equivalent of React Hook Form's `values` prop (+ `resetOptions: { keepDirtyValues: true }`) and Formik's `enableReinitialize`. ```tsx useForm({ values: serverData, keepDirtyValues: true }); ``` Notes: `values` replaces the **whole** value object (full shape, not a partial patch); `isDirty` is measured against `defaultValues`, not the latest `values`; don't put `File`/`Blob` in reactive `values` (deep-compare can't tell two files apart). -------------------------------------------------------------------------------- ## Links - Docs: https://elform.dev/docs/intro - Index for LLMs: https://elform.dev/llms.txt - GitHub: https://github.com/colorpulse6/el-form - npm: https://www.npmjs.com/package/el-form-react - MCP server: `npx el-form-mcp`