> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Forms

> Headless forms for operations, with drafts, validation, file fields and one retained submission

Your application owns inputs, labels, validation messages, focus and layout.
Bijection owns editable drafts, file transfers, validation results and one retained
submission. The SDK supplies no form component or stylesheet and requires no UI
library. The dashboard builds its automatic operation form on this same public API,
through `createOperationClient`. A form loads its file upload code only when a file
is selected, so a bundler that splits dynamic imports keeps that code out of the
initial load.

Every snapshot has an `error`, which says why its owner's latest command or
background step failed. Owners that do something over time, namely operation
calls, forms, file selections, upload entries and upload inputs, also report
what is happening as `state`, which never carries an error. A field holds a
value instead: its snapshot reports the value, its issues and `error`, and a
React field binding exposes that snapshot as `snapshot`. Every command
returns a promise that is already handled: a failure appears on that owner's
`error`, so `onClick={() => void form.recover()}` needs no wrapper, and awaiting
the promise still observes the rejection. No command throws synchronously. Keep
application state only for your own work, such as refreshing a list or
navigating.

## The names you need

| To | Use |
| - | - |
| Keep a draft and submit it from React | `useOperationForm(options)`, which returns an `OperationForm` |
| Bind one control to a field | `useOperationField(field)`, which returns an `OperationFieldBinding` |
| Render part of a form, field or selected file | `useOperationSnapshot(owner, select?)` |
| Render a list's rows | `useOperationItems(field)` |
| Use another framework | `createOperationForm(options)` from `bijection/browser` |
| Send one request without a draft | `useOperation(operation, options)`, in [Calling operations](/operations/calling-operations) |

Reusable controls accept `OperationField<T>` for a typed field, `AnyOperationField`
for any field `useOperationField` can bind, `OperationUploadField` for any field
that selects files, and `AnyOperationForm` for a form of any operation. The other
form types in the [React](/api/modules/react) and [browser](/api/modules/browser)
references name the parts of these.

## Create a form

Use `useOperationForm` to retain editable values, list identities and file selections,
then freeze one exact request on Save:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { useOperationForm, useOperationSnapshot, useOperationField, useOperationItems } from "bijection/react";

const form = useOperationForm({
  client,
  operation: api.reports.createReport,
  draftKey: "new-report",
  initialValues: { title: "", files: [] },
  onAccepted(receipt) {
    navigateToReport(receipt.result);
  },
});

// In an event handler. Each step awaits the one it depends on; a failure also
// appears on the owner's `error`.
await form.field("title").set(title);
const files = form.field("files");
await files.choose(selectedFiles);
const [first] = files.items();
await first.field("caption").set("Train ticket");
await first.move(files.items().length - 1);
await first.remove();
```

`field.choose(files)` validates the whole selection before preparing any file.
The example illustrates commands. In a component, subscribe to the fields you render:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
// Inside your application-owned text control:
const title = useOperationField(form.field("title"));
<label htmlFor={title.id}>Title</label>
<input {...title.getInputProps()} />
<p {...title.issueProps}>
  {title.snapshot.visibleIssues[0]?.message}
  {title.snapshot.error instanceof Error && title.snapshot.error.message}
</p>
```

The reference application's `TextField`, `AttachmentFields`, and `SaveControls`
are application components, not SDK exports. Their implementations live together
in `scripts/fixtures/expense-reports/src`. `TextField` takes a string `field`, a
`label` and an optional `id`. `AttachmentFields` takes a list `field` and a
`label`, and renders its file input, selected files and each item's caption.
`SaveControls` takes the `form`, an optional `label`, `disabled`, `onSubmit`
and `onStartAnother`; without `onSubmit`, Save submits the surrounding native
form. Its `form` is an `AnyOperationForm`, which accepts the form of any
operation with its snapshot, commands and selections but without typed field
navigation. Each selected file renders as an `OperationFile`, which takes an
`owner` (the form or a field) and a `selectionKey`. The file input,
`OperationUpload`, takes an `OperationUploadField`: any field that selects
files, whatever its value shape. A host that finds fields at runtime narrows one
with `isOperationUploadField(field)`. A complete composition, using those helpers
and the generated operation, is:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { useState } from "react";
import { BijectionHttpClient } from "bijection/browser";
import { useOperationForm, useOperationSnapshot } from "bijection/react";
import { api } from "../bijection/_generated/api";
import type { Id } from "../bijection/_generated/dataModel";
import { TextField, AttachmentFields } from "./fields";
import { SaveControls } from "./form-controls";

export function NewReport({
  client,
  onSaved,
}: {
  client: BijectionHttpClient;
  onSaved: (record: Id<"reports">) => Promise<void>;
}) {
  // Application work only; the form shows its own failures on `error`.
  const [presentationError, setPresentationError] = useState<unknown>(null);
  const form = useOperationForm({
    client,
    operation: api.reports.createReport,
    draftKey: "new-report",
    initialValues: { title: "", files: [] },
    onAccepted: async (receipt) => {
      setPresentationError(null);
      try {
        await onSaved(receipt.result);
      } catch (error) {
        setPresentationError(error);
      }
    },
  });
  const canEdit = useOperationSnapshot(form, (snapshot) => snapshot.canEdit);
  const state = useOperationSnapshot(form, (snapshot) => snapshot.state);
  return (
    <form onSubmit={form.handleSubmit}>
      {state.kind !== "accepted" && (
        <fieldset disabled={!canEdit}>
          <TextField field={form.field("title")} label="Title" />
          <AttachmentFields
            field={form.field("files")}
            label="Supporting documents"
          />
        </fieldset>
      )}
      <SaveControls
        form={form}
        onStartAnother={() => void form.startAnother()}
      />
      {presentationError != null && (
        <p role="alert">
          {presentationError instanceof Error
            ? presentationError.message
            : String(presentationError)}
        </p>
      )}
    </form>
  );
}
```

Keep recovery and pending file removals outside the disabled editable fieldset.
`SaveControls` observes the same form, shows its `error` and calls its commands;
it does not maintain another submission state. `onAccepted` owns successful
presentation for both Save and recovery. If `onSaved` fails, the operation
remains accepted and the application shows the presentation error. Its
asynchronous work is not part of the form command's completion. An application
that disables other actions during refresh must retain that busy state until the
refresh finishes. Restoring
a retained request does not replay presentation; checking its current result can
confirm acceptance again.

## Bind controls

The binding generates an ID shared by the input and its issue description. To choose
one yourself, use `useOperationField(form.field("title"), { id: "report-title" })`.
Use `binding.id` for the label; prop getters keep that same ID. Call the hook once
per mounted control, including when two controls edit the same field.

`useOperationField` returns the field's commands, its subscribed `snapshot`, and native
prop getters where the declared field supports them. `getInputProps()` binds text,
checkbox and file inputs; `getTextareaProps()` binds editable text; `getSelectProps()`
binds string values, including literal choices. Text bindings preserve raw text;
checkbox bindings use `checked`; upload bindings use file selection, never a controlled
`value`. Custom controls can call `edit`, `set`, `choose` and `touch` directly;
like the bindings' handlers, their failures appear on `snapshot.error`.
Reusable controls can name each part: a field's `OperationFieldCommands<T>` and
`OperationFieldSnapshot<T>`, which is what every field reports
(`OperationFieldSnapshotBase<T>`) plus what its value's shape adds, such as
`OperationUploadSnapshot`; and a binding's `OperationNativeBindings<F>`, with
`OperationInputOptions`, `OperationInputProps`, `OperationTextareaProps` and
`OperationSelectProps`. `useOperationField` accepts any `AnyOperationField`.
`field.set(value)` assigns an exact typed value; `field.edit(text)` retains unfinished text such as `"-"` for
an integer. `field.advanced.editJson(text)` is the explicit advanced editor and uses the
public JSON value encoding. Snapshots expose the parsed value only when valid,
plus issues and editability. Text-editable fields expose unfinished text; optional
fields expose inclusion; union fields expose their selected alternative. Only upload
fields expose file selections and usable limits (`multiple`, `capacity`, `maxBytes`,
`contentTypes`). Selections expose filenames, `state` with progress, `error` and the
`actions` available now, without controller entries or attachment declarations.
A selection's `path` follows its current argument position; `selectionKey` remains stable when its row moves.
`localFile` is available for an application-owned preview only while this page has
the original bytes. It is never persisted.

Pass application handlers, refs and accessibility descriptions to the getter so they
compose with the binding. Binding handlers run before application handlers, descriptions
are combined, and `disabled: false` cannot enable a frozen field. Values, checkbox
state and file multiplicity remain owned by the field:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
<textarea {...title.getTextareaProps({
  ref: inputRef,
  onBlur: trackBlur,
  "aria-describedby": "title-hint",
})} />
<p id="title-hint">A short description of the report.</p>
```

The composed ref stays stable across field edits and parent renders when the supplied
ref and `focus` function are unchanged. A replaced ref or focus function detaches the
old binding and attaches the new one. Object refs and callback-ref cleanup are supported.

For a value-oriented widget, `getValueProps()` supplies its typed `value` and
`onValueChange(value)` callback. The callback returns `void`; synchronous validation
failures and rejected writes remain on `snapshot.error`. Read `value` as possibly
`undefined` while input is incomplete. A nullable field can be cleared with `null`;
only an optional field accepts `undefined`. The getter performs no value conversion.
Its change callback stays stable while the field handle is unchanged. Native change
and blur handlers also stay stable when the supplied application handlers are
unchanged. Values, issues and editability still follow the current snapshot;
retaining a callback never bypasses a frozen field's command checks.

Native bindings follow the declared shape and available commands, including while
input is incomplete:

| Field | Native binding |
| - | - |
| Editable text, number or exact integer | `getInputProps()`, `getTextareaProps()` preserve unfinished text |
| Boolean, boolean literal or boolean-only union | `getInputProps()` supplies a checkbox |
| String or string-only choice | `getSelectProps()` supplies a string select |
| File selection | `getInputProps()` supplies a file input |

Native changes still pass through field validation. A literal `true` refuses `false`.
Nullable booleans and structured values use `getValueProps()` with a widget that
represents every declared value. Read-only projections expose no mutation bindings.

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const category = useOperationField(form.field("category"));

<Combobox
  {...category.getValueProps()}
  controlProps={category.getControlProps<HTMLButtonElement>()}
  onBlur={category.touch}
/>
<p {...category.issueProps}>
  {category.snapshot.visibleIssues[0]?.message}
  {category.snapshot.error instanceof Error && category.snapshot.error.message}
</p>
```

`Combobox` is an application widget: it forwards `controlProps` to its focusable
anchor, calls `onValueChange` with one declared value, and calls its `onBlur` when
focus leaves the complete widget, including its popup. A widget with a different
value model adapts it explicitly. For example, a date picker returning `Date | null`
can map a UTC calendar date to a nullable ISO date string:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const date = useOperationField(form.field("date"));
const { value, onValueChange } = date.getValueProps();

<DatePicker
  value={value == null ? value : new Date(`${value}T00:00:00Z`)}
  onValueChange={day => onValueChange(day?.toISOString().slice(0, 10) ?? null)}
  controlProps={date.getControlProps<HTMLInputElement>()}
  onBlur={date.touch}
/>
```

For unfinished text, use `edit` instead; it preserves input such as `"-"` without
inventing a parsed value. Call and await `set` or `edit` directly when subsequent
work depends on a successful write.

Spread `getControlProps()` on the widget's anchor. It supplies editability,
focus registration and issue associations, preserving supplied event handlers.
Native input, textarea and select getters automatically touch on blur. Custom
anchors leave interaction completion to their widget. An explicit `focus` function can direct validation focus
into an editor or portal; its anchor must be mounted inside the native form.
For a popup rendered outside the anchor's DOM subtree, call `touch()` when the
user leaves the complete widget, according to that widget's focus lifecycle:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const focusEditor = useCallback(() => editorRef.current?.focus(), []);

<div {...title.getControlProps({ focus: focusEditor })}>
  <RichTextEditor
    ref={editorRef}
    value={title.snapshot.text}
    onChange={text => void title.edit(text)}
    onBlur={title.touch}
  />
</div>
<p {...title.issueProps}>
  {title.snapshot.visibleIssues[0]?.message}
  {title.snapshot.error instanceof Error && title.snapshot.error.message}
</p>
```

## Reusable controls

Build reusable application controls around typed field handles. Each control owns
its subscription, markup and messages:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import type { OperationField } from "bijection/browser";

function TextField({ field, label, id }: {
  field: OperationField<string>;
  label: string;
  id?: string | undefined;
}) {
  const binding = useOperationField(field, { id });
  return <div>
    <label htmlFor={binding.id}>{label}</label>
    <input {...binding.getInputProps()} />
    <p {...binding.issueProps}>
      {binding.snapshot.visibleIssues[0]?.message}
      {binding.snapshot.error != null && String(binding.snapshot.error)}
    </p>
  </div>;
}

// Inside the report form:
<TextField field={form.field("title")} label="Title" />
```

The same composition applies to object fields. For example, an address group can
be reused wherever the operation accepts that shape:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
function AddressFields({ field }: {
  field: OperationField<{ street: string; city: string }>;
}) {
  return <fieldset>
    <legend>Address</legend>
    <TextField field={field.field("street")} label="Street" />
    <TextField field={field.field("city")} label="City" />
  </fieldset>;
}
```

An object field or collection item keeps the parent's draft, validation and submission
owner. A group needs no new form instance, provider or repeated validation schema.
Keep collection subscriptions in their list component and field subscriptions in
the controls; each then updates for the state it renders.

Commands follow the declared kind: objects expose `field(name)`, lists expose
`items()`, `item(itemKey)` and `append(...values)`, and records expose `add(name, value)`
and stable entries with `rename(propertyName)`. Optional fields expose `include(boolean)`;
unions expose `selectAlternative(index)`. File selection appears only on a declared
upload or a collection with one unambiguous upload route. A plain text field has
no `choose`, `move` or `remove` command.

## Subscribe to what you render

`useOperationForm` returns a stable handle without subscribing its component to
state. Subscribe where the result is rendered. `useOperationSnapshot` subscribes
to a form, a field or a file selection, with an optional selector:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const canSubmit = useOperationSnapshot(form, state => state.canSubmit);
const state = useOperationSnapshot(form, snapshot => snapshot.state);
// Your native form owns its markup:
<form onSubmit={form.handleSubmit}>
  {/* Application-owned field components use useOperationField. */}
  <button disabled={!canSubmit}>Save</button>
</form>
```

Selectors compare results with `Object.is`; unchanged built-in collection snapshots
keep their identity. For a derived object or array, pass a comparator explicitly:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const availability = useOperationSnapshot(
  form,
  state => ({ canSubmit: state.canSubmit, working: state.working }),
  { compare: (a, b) => a.canSubmit === b.canSubmit && a.working === b.working },
);
```

A whole-draft editor can explicitly select the entire snapshot. Place
`useOperationField` in the control component so unrelated controls stay quiet.

## Acceptance and validation

`onAccepted(receipt)` observes confirmed acceptance after a mounted form's `submit`,
`retry`, `recover` or `retryDraftWrite` command releases its task. It runs once per
request on that form instance, uses the latest committed callback, and may navigate
or call `startAnother()`. Restoration alone does not invoke it; an explicit recovery
after reload can confirm acceptance and notify the newly mounted form. Unmounting or
changing the draft suppresses pending notifications. This is a presentation callback,
not a durable side-effect handler. Catch callback failures to display them; otherwise
they are reported to the console. They cannot reject an accepted submission, change
its receipt, or trigger another request.

Validation remains accurate from initialization. `issues` contains all local
findings; `visibleIssues` reveals them after blur or a submission attempt and updates
them during editing. `touched` means the user has left the control; `dirty` compares
its current input with the retained initial input, including unfinished text.
Reload preserves that baseline even when the caller supplies new defaults.
Discard restores it and clears interaction state. Row interaction follows stable
item identities through reordering.

Native bindings supply IDs, names, refs, edit availability, blur/change handlers
and issue associations. Render the issue element using `issueProps` as above.
`handleSubmit` prevents native navigation, retains failures, and focuses the first
mounted invalid control within that form. `submit()` is the programmatic command;
`revealIssues()` also supports a preview without dispatch. Applications own markup,
labels, styling, custom widgets and message presentation. Advanced editors use
`form.advanced.editDraft`, `form.advanced.fieldAt` and `snapshot.advanced.fieldDraft`;
field validators and structural paths are under `binding.snapshot.advanced` in React.

## Lists

`field.items()` returns an immutable collection of stable row handles. Its identity
changes only with membership or order. Subscribe to it directly in React:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const items = useOperationItems(form.field("files"));
items.map(item => <AttachmentRow key={item.itemKey} field={item} />);
```

Each application-owned row subscribes to its own fields; editing a caption does not
change the collection. `field.item(row.itemKey)` finds the
same row after reordering; a removed row's handle cannot edit a later row.

## File fields

Selection keys identify uploads independently of row positions. A native file input
can select files without a UI dependency:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const files = form.field("files");
const uploads = useOperationField(files);
<label htmlFor={uploads.id}>Documents</label>
<input {...uploads.getInputProps()} />
<p {...uploads.issueProps}>
  {uploads.snapshot.visibleIssues[0]?.message}
  {uploads.snapshot.error instanceof Error && uploads.snapshot.error.message}
</p>
```

Render each `uploads.snapshot.selections` entry using its filename, `state`, `error`
and `actions`. A selection's `state` is an upload state, `loading` while its
uploads are restored, or `failed` with `operation: "load"` when restoring them
failed. Its `error` is its latest failure: a command's refusal or failure, the
file's check or transfer, or restoring its upload. A command clears its own
earlier failure when it starts. Its `actions` are `AttachmentAction` values: the file's own
and those for the whole input.
`form.selection(selectionKey)` or `field.selection(selectionKey)` returns an
`OperationSelection`, a stable handle with `getSnapshot()`, `subscribe(listener)`
and one command per action kind: `pause()`, `resume()`, `choose(file)`,
`retry()`, `discard()`, `retryLoad()` and `forgetLocalRecovery()`. A custom React
file row subscribes with `useOperationSnapshot(selection)` and renders one
control per entry in `actions`, disabled when the action is; the
[actions table](/file-storage/upload-files#upload-while-the-user-edits) lists typical controls. A
`choose` action's `requirement` says whether the original file or another one
is needed; pass the chosen bytes to `choose(file)`, and the controller checks
the required identity. The form applies its own editability to each action's
`disabled`. `forgetLocalRecovery()` explicitly clears unreadable upload
recovery; it neither cancels a remote upload nor forgets the form’s Save
request. The reference application's `OperationFile` renders a selection this
way.

Removing a row retains its cancellation intent before hiding it. Render the
selections whose `removing` is true, `snapshot.selections.filter((s) => s.removing)`,
outside disabled editing controls so a user can retry
`form.selection(selectionKey).discard()`; it stays available while the draft is
frozen, unless the form is working. Unresolved removals survive reload and block
editing and Save. The application renders recovery with the commands below.

## Save and recovery

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const receipt = await form.submit();
// The local business commit is confirmed. Render/open its result.
await form.startAnother(); // Start another draft after confirmation.
```

`form.discard()` clears editable input back to its initial values and durably
reconciles selected files. Pending removals survive reload and remain visible.
It is unavailable during an unconfirmed Save. Forget local recovery is a separate
advanced action and never claims to cancel remote work.

Save retains its exact request before dispatch. While `state.kind === "uncertain"`,
keep editing disabled and show **Save not confirmed**. **Check result** calls
`await form.recover()` and never submits; **Retry save** calls `await form.retry()`
with the original arguments, contract and request key. An absent result does not
make the draft editable. A confirmed result is `state.kind === "accepted"`.
`startAnother()` releases local selection bookmarks and starts a fresh private creation
context; accepted request recovery remains retained.

`draftKey` identifies editable work, contains 1–128 UTF-8 bytes and stays stable
across renders and reloads. Keep one client per signed-in account and replace it
when that account changes. Initial values initialize each draft identity; later
prop changes do not overwrite retained edits. Drafts are encrypted and scoped to
the tab, deployment, verified account, operation and draft key. They contain no
file bytes or credentials. Each record is limited to 512 KiB, 4,096 tree nodes
across its current input, initial baseline and recovery structure, and depth 32, in addition to the operation's upload limits.

Malformed or inaccessible recovery data produces `state.kind === "blocked"`.
Keep it and show the cause. A failed editable-draft or request-link write can use
`retryDraftWrite()`. A request-link retry retains the same local recovery link
before checking the result; it cannot dispatch until that write succeeds. Explicit `forgetLocalRecovery()` clears local recovery only;
it neither cancels an uncertain Save nor discards remote preparations. Check the
saved business records before offering it. Transport errors never authorize a
new Save. Throw `OperationRefusal` from preparation to record a terminal refusal,
such as a stale report revision. The engine rolls back business writes and fences
that request key. A checked native refusal restores the original editable fields
and file selections, including after a lost reply and reload. No application error
predicate is needed. Timeouts, temporary access failures and an absent recovery
result keep the request frozen.

## Other frameworks and advanced editors

For another framework, `createOperationForm` from `bijection/browser` exposes
these commands with `getSnapshot()`, `subscribe(listener)` and `start()`.
Call `start()` once per mount and its returned cleanup on unmount. Rendering or
constructing a controller starts no remote work. The React hook supplies this
lifecycle. The dashboard uses these public APIs for its own object and geometry
controls; it has no private renderer bridge into the SDK.

Advanced schema-driven editors can inspect `field.getSnapshot().advanced.validator` and
`advanced.fieldDraft`, and call `field.advanced.editDraft(update)` or `form.advanced.editDraft(update)` to
retain incomplete input exactly. `createFieldDraft`, `fieldDraftFromValue`,
`parseFieldDraft` and `updateFieldDraftAt` from `bijection/browser` share the draft
model and value encoding. `form.advanced.fieldAt(identityPath)` navigates object names and
stable item keys; union alternatives add no path segment. Ordinary application
inputs use `set`, `edit`, `choose` and the typed row commands above.

`initialFieldDraft` initializes an advanced editor; `fieldShape` can refine an
`any` argument for local input. Neither changes the admitted operation contract:
submission still validates that contract. `initialContext`, `form.setContext(value)`
and `form.context` retain application-owned draft context, such as the revision
from which defaults were read. Context grants no authority and never replaces
business arguments. All draft edits keep the same retention bounds and file
ownership checks; editors cannot fabricate or move selections outside declared
upload fields.

The [expense-report example](https://github.com/bijectionhq/bijection/tree/main/scripts/fixtures/expense-reports)
contains the complete controls for editing, upload recovery, uncertain Save,
replacement and removal. The lower-level
[upload controller](/file-storage/upload-files#upload-while-the-user-edits) remains
useful when a host already owns its editable draft and submission lifecycle.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.