Skip to main content
form() creates a type-safe form instance bound to a generated SDK mutation method. It provides reactive field state, schema validation, progressive enhancement attributes (action, method), and per-field error tracking. The SDK method carries the validation schema in its metadata, so form() picks it up automatically — the Result type is handled internally. When the mutation returns { ok: false }, field errors are extracted and mapped to the form fields.

form()

import { form } from '@vertz/ui/form';

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

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

      <textarea name={taskForm.fields.description} />
      {taskForm.description.error && <span>{taskForm.description.error}</span>}

      <button type="submit" disabled={taskForm.submitting}>
        Create
      </button>
    </form>
  );
}

Signature

function form<TBody, TResult>(
  sdkMethod: SdkMethod<TBody, TResult>,
  options?: FormOptions<TBody, TResult>,
): FormInstance<TBody, TResult>;

FormInstance

The returned instance has two kinds of properties: reserved form properties and per-field accessors.

Form properties

PropertyTypeDescription
actionstringURL for the form’s action attribute (progressive enhancement)
methodstringHTTP method for the form’s method attribute
onSubmit(e: Event) => Promise<void>Submit handler — validates, then calls the SDK method
submittingbooleantrue while the submission is in flight
dirtybooleantrue if any field has been modified
validbooleantrue if all fields pass validation
reset()() => voidReset all fields to initial values
submit()(formData?: FormData) => Promise<void>Programmatic submit
setFieldError()(field: string, message: string) => voidSet a custom error on a field

Per-field accessors

Access any field by name on the form instance (e.g., taskForm.title). Each field is a FieldState:
PropertyTypeDescription
valueTCurrent field value
errorstring | undefinedValidation error message
dirtybooleantrue if the field has been modified
touchedbooleantrue if the field has been focused and blurred
setValue()(value: T) => voidUpdate value and mark dirty
reset()() => voidReset to initial value, clear error/dirty/touched

FormOptions

OptionTypeDefaultDescription
schemaFormSchema<TBody>Validation schema (auto-inferred from SDK metadata when available)
initialPartial<TBody> | () => Partial<TBody>Initial field values
onSuccess(result: TResult) => voidCallback after successful submission
onError(errors: Record<string, string>) => voidCallback on validation or submission error
resetOnSuccessbooleanfalseReset fields after successful submission
revalidateOn'blur' | 'change' | 'submit''blur'When to re-validate fields with errors after first submission

Progressive enhancement

The action and method properties enable the form to work without JavaScript. When JS is available, onSubmit intercepts the submission and handles it client-side with validation.
<form action={taskForm.action} method={taskForm.method} onSubmit={taskForm.onSubmit}>
  {/* Works with and without JS */}
</form>

Types

interface FormOptions<TBody, TResult> {
  schema?: FormSchema<TBody>;
  initial?: Partial<TBody> | (() => Partial<TBody>);
  onSuccess?: (result: TResult) => void;
  onError?: (errors: Record<string, string>) => void;
  resetOnSuccess?: boolean;
  revalidateOn?: 'blur' | 'change' | 'submit';
}

type FormInstance<TBody, TResult> = {
  action: string;
  method: string;
  onSubmit: (e: Event) => Promise<void>;
  reset: () => void;
  setFieldError: (field: keyof TBody & string, message: string) => void;
  submit: (formData?: FormData) => Promise<void>;
  submitting: boolean;
  dirty: boolean;
  valid: boolean;
} & { [K in keyof TBody]: FieldState<TBody[K]> };

interface FieldState<T = unknown> {
  value: T;
  error: string | undefined;
  dirty: boolean;
  touched: boolean;
  setValue: (value: T) => void;
  reset: () => void;
}

interface FormSchema<T> {
  parse(data: unknown): { ok: true; data: T } | { ok: false; error: unknown };
}