form() connects an HTML form to any endpoint with client-side validation, per-field error tracking, and progressive enhancement. It works with generated SDK methods (entity CRUD), custom service endpoints, or any function that matches the SdkMethod interface — forms are not limited to entities.
Basic usage
When using a generated SDK method,
form() automatically extracts the validation schema from the
method’s metadata — no need to pass schema manually. You only need to provide a schema
explicitly when using a custom (non-generated) SDK method.How it works
form()takes an SDK method (from your typed API client) and optional config- On submit, it validates against the schema before sending the request
- Validation errors are available per-field via
taskForm.fieldName.error - On success, calls
onSuccesswith the API response - On server error, sets a
_formerror
Field access
Access per-field state via the form instance using the field name:Validation
Schema validation
Pass a schema to validate before submission:parse method:
@vertz/schema parsers. Validation errors are automatically mapped to per-field errors.
Manual field errors
Set errors programmatically:FormData coercion
FormData only carries strings. form() coerces each value to the type declared by the body schema before validation runs, so plain s.boolean(), s.number(), s.bigint(), and s.date() work directly on <input> values — no s.coerce.* workaround needed.
| Schema leaf | FormData input | Coerced value |
|---|---|---|
s.boolean() | checkbox checked (any non-empty string) | true |
s.boolean() | checkbox absent (unchecked) | false |
s.boolean() | explicit "false", "0", "off" | false |
s.number() | "42" | 42 |
s.number() | "" (empty) | dropped — field is treated as missing (lets optional() validate) |
s.bigint() | "9007199254740993" | 9007199254740993n |
s.date() | "2026-04-18" | new Date("2026-04-18") |
s.string() | "42" | "42" (never coerced) |
s.array(s.string()) | repeated name="tags" checkboxes | string[] from all selected values |
Coercion only applies to leaves where the schema declares a primitive type. Arrays of objects fall back to FormData’s dotted-index parsing without per-leaf coercion — file uploads via
s.instanceof(File) are unchanged.Top-level .refine() and .superRefine() are walked through automatically. Other top-level wrappers (.transform(), .pipe(), .catch(), .brand(), .readonly()) currently disable coercion — wrap your s.object(...) with these only at the field level for now.Form-level state
Initial values
Pre-populate fields with initial values:Callbacks
Programmatic submission
Submit without a form element:Progressive enhancement
Theaction and method properties enable forms that work without JavaScript:
onSubmit intercepts and handles it client-side with validation.
Field revalidation
By default, fields with errors are revalidated when the user blurs (leaves) the field — giving immediate feedback without validating fields the user hasn’t interacted with yet. Control this with therevalidateOn option:
| Value | Behavior |
|---|---|
'blur' (default) | Re-validates errored fields when the user blurs them |
'change' | Re-validates errored fields on every input or change event |
'submit' | No re-validation between submissions — errors only update on submit |
Reset
Reset all fields to their initial values:Forms without entities
form() works with any function that matches the SdkMethod interface — it is not limited to entity CRUD methods. Use it for search filters, settings panels, contact forms, or any scenario where you need validation, field state tracking, and progressive enhancement.
When to use form() without entities
- Search / filter forms — validate filter inputs, track dirty state, submit to a custom endpoint
- Settings panels — schema-validated preferences with per-field errors and reset
- Contact / feedback forms — progressive-enhanced forms that submit to a service action
- Any custom endpoint — anything exposed via
service()or a manualSdkMethod
Using with service actions
Service actions from the generated SDK work the same as entity methods:Using with a custom SdkMethod
When you need a form for an endpoint that isn’t generated by codegen, create an SdkMethod manually. You must provide a schema since there’s no metadata to infer it from:
Settings panel example
When NOT to use form()
For simple UI-only state that doesn’t submit to an endpoint (e.g., a local filter toggle), reactive variables are simpler:
form() when you need validation, per-field error tracking, dirty/submitting state, or progressive enhancement. For pure client-side state without submission, let variables are enough.
Form-level onChange
<form onChange={handler}> fires a callback with all current form values whenever any child input changes. The handler receives a FormValues object — a flat { [key: string]: string } snapshot — instead of a DOM Event.
onInput handlers.
Per-input debounce
Adddebounce={ms} to any <input>, <textarea>, or <select> to delay the onChange callback for that field:
- Text inputs with
debouncewait for the user to stop typing before firing - Inputs without
debouncefire immediately (coalesced via microtask batching) - When an immediate change occurs (e.g., select change), all pending debounce timers are canceled and flushed together
How it works
- The compiler transforms
<form onChange={handler}>into__formOnChange(form, handler)— a delegated event listener on the form debounce={N}on inputs becomesdata-vertz-debounce="N"— the runtime reads this attribute- Non-debounced events are coalesced via microtask batching (multiple changes in the same tick = one callback)
- The callback receives
FormValuesfromnew FormData(form), giving a point-in-time snapshot
Limitations
- String values only — all values in
FormValuesare strings (from FormData serialization) - Unchecked checkboxes absent — unchecked checkboxes are not included in FormData (standard HTML behavior)
- No multi-value fields — multiple values for the same name (e.g., multi-select) are not supported; only the last value is kept
Escape hatch: raw DOM events
If you need the native DOMchange event (e.g., for file inputs or custom behavior), use a ref:
Interaction with form()
form() and <form onChange> serve different purposes and can be used together:
form()— handles submission, validation, per-field errors, and server communication<form onChange>— reacts to input changes in real time (search-as-you-type, live filtering)