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
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 and browser
references name the parts of these.
Create a form
UseuseOperationForm to retain editable values, list identities and file selections,
then freeze one exact request on Save:
field.choose(files) validates the whole selection before preparing any file.
The example illustrates commands. In a component, subscribe to the fields you render:
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:
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, useuseOperationField(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:
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:
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.
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:
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:
Reusable controls
Build reusable application controls around typed field handles. Each control owns its subscription, markup and messages: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:
Object.is; unchanged built-in collection snapshots
keep their identity. For a derived object or array, pass a comparator explicitly:
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:
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: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 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
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
contains the complete controls for editing, upload recovery, uncertain Save,
replacement and removal. The lower-level
upload controller remains
useful when a host already owns its editable draft and submission lifecycle.