// The binding itself: an action's input schema on one side, `Field`'s error slot on the other, and // a submit that only the SERVER can turn into a success. // // Server authority is structural here, not documented: `submit` is required, `succeeded` is // produced in exactly one place — after the caller's `submit` resolves — and the local parse's // VALUE is discarded, so a client cannot decide either that a form is valid or what it said. import { invalidFieldPathError } from '../errors'; import { parseFieldPath } from './field-path'; import { type FormIssue, type FormSchema, issuesFromRejection, issuesFromValidation, } from './form-issue'; import { distributeIssues, errorOf, type FormState, firstInvalidField, IDLE_FORM_STATE, messagesOf, NO_FORM_ERRORS, } from './form-state'; import { type FormTouch, markDirty, markTouched, NO_FORM_TOUCH, sameFieldValue, } from './form-touch'; export interface FormBindingOptions { /** * The server call — `action.client()`, `rpc(...).createPost`, or a `fetch` that throws on a * refusal. REQUIRED: it is the only thing in this file that can produce a success. */ readonly submit: (values: TValues) => Promise; /** The paths this form renders a control for. An issue matching none surfaces at the form. */ readonly fields: readonly string[]; /** The app's wording for one issue. The framework ships the mapping, never the copy. */ readonly messageFor: (issue: FormIssue) => string; /** * The action's `input`. Optional, and a LATENCY optimisation only: the action re-parses on the * server on every path, so omitting this changes when the user hears about a bad value, never * whether it is rejected. */ readonly schema?: FormSchema | undefined; /** Called on every transition — how a reactive shell mirrors the state into a signal. */ readonly onState?: ((state: FormState) => void) | undefined; /** * The values the form OPENED with, keyed by the same field paths — what `edit()` compares * against to decide dirtiness. A create form passes nothing, and every non-blank value is then a * change. A path missing from this map has a baseline of `undefined`, which `sameFieldValue` * treats as equal to an empty control. * * The baseline is fixed for the life of the binding: a form that stays mounted after a save and * wants the saved values as its new baseline builds a new binding, because this one never sees * what the user typed and cannot invent one. */ readonly initial?: Readonly> | undefined; } export interface FormBinding { readonly state: () => FormState; readonly submit: (values: TValues) => Promise>; /** The message `Field`'s single error slot renders for one path. */ readonly errorFor: (path: string) => string | undefined; /** Every message bound to one path, when a form renders more than one. */ readonly messagesFor: (path: string) => readonly string[]; /** * A submit is in flight. The one value that reaches BOTH the submit control (`