alepha@docs:~/docs/guides/frontend$
cat 4-forms.md
4 min read
Last commit:

#Forms

Alepha provides useForm for schema-driven forms with TypeBox validation, automatic input generation, and lifecycle events.

#Basic Usage

typescript
 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 identifier
  • noValidate -- set to true (validation is handled by the schema)
  • onSubmit -- calls form.submit() with preventDefault
  • onReset -- calls form.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:

typescript
 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:

typescript
 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:

typescript
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:

typescript
 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).