Skip to main content
form() connects an HTML form to your API with client-side validation, per-field error tracking, and progressive enhancement.

Basic usage

import { form } from 'vertz/ui';
import { api } from '../client';

export function CreateTaskForm({ onSuccess }: CreateTaskFormProps) {
  const taskForm = form(api.tasks.create, { onSuccess });

  return (
    <form action={taskForm.action} method={taskForm.method} onSubmit={taskForm.onSubmit}>
      <input name={taskForm.fields.title} placeholder="Task title" />
      {taskForm.title.error && <span>{taskForm.title.error}</span>}

      <button type="submit" disabled={taskForm.submitting}>
        Create
      </button>
    </form>
  );
}
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

  1. form() takes an SDK method (from your typed API client) and optional config
  2. On submit, it validates against the schema before sending the request
  3. Validation errors are available per-field via taskForm.fieldName.error
  4. On success, calls onSuccess with the API response
  5. On server error, sets a _form error

Field access

Access per-field state via the form instance using the field name:
const taskForm = form(api.tasks.create, { schema });

// Per-field reactive properties
taskForm.title.error; // Validation error message (signal)
taskForm.title.value; // Current value (signal)
taskForm.title.dirty; // Changed from initial value (signal)
taskForm.title.touched; // User has focused and blurred (signal)

// Methods
taskForm.title.setValue('New title');
taskForm.title.reset();
All field properties are signals — they auto-unwrap in JSX:
// In JSX — no .value needed
<span>{taskForm.title.error}</span>
<button disabled={taskForm.submitting}>Submit</button>

Validation

Schema validation

Pass a schema to validate before submission:
const taskForm = form(api.tasks.create, {
  schema: createTaskSchema,
});
The schema must implement a parse method:
interface FormSchema<T> {
  parse(data: unknown): { ok: true; data: T } | { ok: false; error: unknown };
}
This is compatible with vertz/schema parsers. Validation errors are automatically mapped to per-field errors.

Manual field errors

Set errors programmatically:
taskForm.setFieldError('title', 'Title already exists');

Form-level state

taskForm.submitting; // true while the API call is in progress
taskForm.dirty; // true if any field has changed from initial values
taskForm.valid; // true if no field errors exist

Initial values

Pre-populate fields with initial values:
const editForm = form(api.tasks.update, {
  schema: updateTaskSchema,
  initial: {
    title: task.title,
    description: task.description,
  },
});
Or use a function for dynamic initial values:
const editForm = form(api.tasks.update, {
  schema: updateTaskSchema,
  initial: () => ({
    title: task.data?.title ?? '',
  }),
});

Callbacks

const taskForm = form(api.tasks.create, {
  schema: createTaskSchema,

  // Called with the API response on success
  onSuccess: (result) => {
    navigate({ to: '/tasks/:id', params: { id: result.id } });
  },

  // Called with field error map on validation failure
  onError: (errors) => {
    console.log('Validation failed:', errors);
  },

  // Reset all fields after successful submission
  resetOnSuccess: true,
});

Programmatic submission

Submit without a form element:
await taskForm.submit();

// Or with custom FormData
const formData = new FormData();
formData.set('title', 'My task');
await taskForm.submit(formData);

Progressive enhancement

The action and method properties enable forms that work without JavaScript:
<form
  action={taskForm.action}
  method={taskForm.method}
  onSubmit={taskForm.onSubmit}
>
Without JS, the form submits directly to the API endpoint. With JS, 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 the revalidateOn option:
const taskForm = form(api.tasks.create, {
  schema: createTaskSchema,
  revalidateOn: 'blur', // default — revalidate on blur
});
ValueBehavior
'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
Revalidation only activates after the first form submission. Before the user submits, no field-level validation feedback is shown. This avoids premature error messages on untouched forms. Only fields that already have errors are re-validated — fields without errors are not checked until the next submission.

Reset

Reset all fields to their initial values:
<button type="button" onClick={() => taskForm.reset()}>
  Cancel
</button>