#Forms
Alepha provides useForm for schema-driven forms with TypeBox validation, automatic input generation, and lifecycle events.
#Basic Usage
1import { t } from "alepha"; 2import { useForm } from "alepha/react/form"; 3 4function LoginForm() { 5 const form = useForm({ 6 schema: t.object({ 7 email: t.email(), 8 password: t.text(), 9 }),10 handler: async (values) => {11 await api.login(values);12 },13 });14 15 return (16 <form {...form.props}>17 <input {...form.input.email.props} />18 <input {...form.input.password.props} type="password" />19 <button type="submit">Login</button>20 </form>21 );22}
Spread form.props on the <form> element and form.input.<field>.props on each input. The props include name, type, onChange, required, defaultValue, and other attributes derived from the schema.
#useForm Options
| Option | Type | Description |
|---|---|---|
schema |
TObject |
TypeBox schema defining fields and validation. |
handler |
(values, { form }) => unknown |
Called on submit with validated values. |
initialValues |
Partial<Static<T>> |
Pre-populate fields with existing data. |
id |
string |
Prefix for field IDs and data-testid attributes. |
onChange |
(key, value, store) => void |
Called on every field change. |
onError |
(error, { form }) => void |
Called when submission throws an error. |
onReset |
() => void |
Called when the form is reset. |
onCreateField |
(name, schema) => InputHTMLAttributes |
Customize generated input attributes. |
The second argument to useForm is a dependency array (defaults to []). When dependencies change, the form is re-created.
#FormModel
useForm returns a FormModel<T> instance with the following API:
#form.props
Spread on the <form> element. Includes:
id-- unique form identifiernoValidate-- set totrue(validation is handled by the schema)onSubmit-- callsform.submit()withpreventDefaultonReset-- callsform.reset()
#form.input
A proxy object where each key corresponds to a schema property. Each field has:
| Property | Type | Description |
|---|---|---|
props |
InputHTMLAttributes |
Spread on the <input> element. |
path |
string |
JSON pointer path (e.g., /email). |
required |
boolean |
Whether the field is required. |
schema |
TSchema |
The TypeBox schema for this field. |
set |
(value: any) => void |
Programmatically set the field value. |
form |
FormModel |
Reference back to the parent form. |
#form.submit()
Triggers form submission programmatically. Validates values against the schema, then calls the handler. Prevents concurrent submissions.
#form.reset(event)
Clears all form values and emits a form:reset event.
#form.currentValues
Returns the current form values as a restructured object (nested keys like address.city become { address: { city: ... } }).
#form.submitting
Boolean indicating if a submission is in progress.
#Automatic Type Detection
Input types are automatically inferred from the schema:
| Schema Type | Input Type |
|---|---|
t.integer() |
number |
t.number() |
number |
t.boolean() |
checkbox |
t.email() |
email |
t.text() |
text |
Field named password |
password |
Field named url |
url |
t.string({ format: "date" }) |
date |
t.string({ format: "time" }) |
time |
t.string({ format: "date-time" }) |
datetime-local |
t.string({ format: "binary" }) |
file |
String constraints like maxLength and minLength are also applied to the input attributes.
#Nested Object Fields
For schemas with nested objects, use items to access child fields:
1const form = useForm({ 2 schema: t.object({ 3 address: t.object({ 4 street: t.text(), 5 city: t.text(), 6 }), 7 }), 8 handler: async (values) => { /* values.address.street, values.address.city */ }, 9});10 11// Access nested fields:12<input {...form.input.address.items.street.props} />13<input {...form.input.address.items.city.props} />
#Tracking Form State
Use useFormState to reactively track loading, dirty, error, and value states:
1import { useFormState } from "alepha/react/form"; 2 3function MyForm() { 4 const form = useForm({ /* ... */ }); 5 const { loading, dirty, error } = useFormState(form); 6 7 return ( 8 <form {...form.props}> 9 {/* inputs */}10 {error && <p>{error.message}</p>}11 <button type="submit" disabled={loading || !dirty}>12 {loading ? "Saving..." : "Save"}13 </button>14 </form>15 );16}
useFormState options:
The first argument is the form model (or { form, path } to track a specific field). The second argument is an array of keys to track:
1// Track only loading and error2const { loading, error } = useFormState(form, ["loading", "error"]);3 4// Track a specific field's error5const { error } = useFormState({ form, path: "/email" }, ["error"]);6 7// Track current values8const { values } = useFormState(form, ["values"]);
Return type:
| Property | Type | Description |
|---|---|---|
loading |
boolean |
True during form submission. |
dirty |
boolean |
True after any field change. Resets on successful submit. |
error |
Error | undefined |
Error from the last failed submit. |
values |
Record | undefined |
Current form values (updated on change and submit). |
#Form Events
Forms emit events on the Alepha event system:
| Event | Payload | Description |
|---|---|---|
form:change |
{ id, path, value } |
A field value changed. |
form:reset |
{ id, values } |
Form was reset. |
form:submit:begin |
{ id } |
Submission started. |
form:submit:success |
{ id, values } |
Submission succeeded. |
form:submit:error |
{ id, error } |
Submission failed. |
form:submit:end |
{ id } |
Submission finished (always). |
Forms also emit react:action:begin, react:action:success, react:action:error, and react:action:end events with type: "form", so global action handlers apply to form submissions too.
#FormValidationError
Throw a FormValidationError in your handler to report field-level validation errors:
1import { FormValidationError } from "alepha/react/form"; 2 3handler: async (values) => { 4 const exists = await api.checkEmail(values.email); 5 if (exists) { 6 throw new FormValidationError({ 7 message: "Email already in use", 8 path: "/email", 9 });10 }11}
The path is a JSON pointer matching the field path (e.g., /email, /address/city).