keyed_form 0.1.0 copy "keyed_form: ^0.1.0" to clipboard
keyed_form: ^0.1.0 copied to clipboard

A form-state controller for immutable aggregates, built on keyed_form_core: holds the working draft, FieldKey-addressed validation errors and touched/dirty/submit state.

keyed_form #

A form-state controller for immutable aggregates. Pure Dart, no Flutter dependency.

KeyedFormController<Root> owns the editable draft, the field-keyed validation errors, and the touched/dirty/revealed bookkeeping that decides when an error is shown. Reads, writes, validation lookups and dirty checks all speak the FieldRef vocabulary from keyed_form_core, re-exported here — so UI code addresses a field the same way whether it is reading it, writing it, or asking for its error.

Observability is ChangeNotifier from package:listen (the official Flutter-team observable package), so the controller can live in a plain Dart test, a CLI, or — via keyed_form_flutter — a widget tree.

Usage #

final form = KeyedFormController<InvoiceForm>(
  initialValue: const InvoiceForm(),
  mode: KeyedFormMode.onTouched,
  resolver: InvoiceForm.validateData, // generated by keyed_form_gen
);

form.field(InvoiceFields.customerEmail).set('ada@example.com');
form.field(InvoiceFields.customerEmail).error; // null until touched / submitted

form.validate(); // whole-draft validation, reveals every error

await form.submit(
  (value) => api.save(value), // runs only if valid; toggles `submitting`
  onInvalid: (errorKeys) => print('rejected: $errorKeys'),
);

form.field(ref) returns a FieldHandle — a statically-typed per-field facade (set / update / value / error / dirty / touch(), and list() for a list field). It is the everyday way in and out of a field; .set(value) rejects a wrongly-typed value at compile time.

A field whose validation is a server round-trip (checking an email isn't already taken) toggles its own isValidating flag around the check:

await form.field(InvoiceFields.customerEmail).validateAsync(
  () => api.checkEmailAvailable(form.field(InvoiceFields.customerEmail).value),
);

Reading the controller's getters (form.value, form.field(x).value, form.isDirty) is non-reactive — the getValues of this family, for event handlers. To watch a slice in a widget's build, use keyed_form_flutter's context.watchField / context.watchForm / context.selectForm.

Pieces #

Piece What it is
KeyedFormController<Root> Owns the draft, errors, touched, revealed, submitted/submitting; field(ref), touch, validate/validateScopes, submit(onValid, {onInvalid}), seed/reset, setServerErrors/setServerErrorPaths
FieldHandle<Root, V> What form.field(ref) returns — set/update, value/error/dirty/key, touch(), list() for a list field
validateAsync(check, {timeout, onFailure}) A field's own async check (e.g. against a server) — see below
markReadOnly() / unmarkReadOnly() / isReadOnly Freeze a field against writes without affecting validation — see below
form.addRelation(source, select, onChange) Derive one field's value from another — see below
KeyedFormMode When a field's error becomes visible (onChange/onBlur/onTouched/onSubmit/all)
KeyedFormResolver<Root> / KeyedFormScopeOf Validation function (draft, scope) => FieldErrors<String>, and an optional written-field-to-subtree mapper
KeyedFormList<Root, Item> By-id editor for one list field — append/insert/removeById/move/updateById, …; obtain one with form.field(ref).list()
KeyedFormSnapshot<Root> Immutable, ==-comparable point-in-time copy of the controller's coarse state, for a host that wants a value rather than a listenable

KeyedFormMode never hides an error a submit attempt surfaced, nor one explicitly revealed — the mode only governs visibility before one of those.

validateAsyncisValidating is true while the check runs. A thrown check, or one that exceeds timeout, sets isFailedValidation instead of writing an error or propagating — a technical fault ("couldn't check the value"), kept separate from errors ("the value is invalid") and cleared by the next validateAsync call, not sticky.

Read-only — freezing a key also freezes, by FieldKey ancestor coverage, everything nested under it. Pass force: true to set/update to write through the freeze. Read-only is configuration: it survives seed()/reset(), unlike touched/revealed/validating/failed.

addRelation — calls onChange with the selected slice of source whenever it actually changes; registering it does not itself call onChange. Returns a callback to unsubscribe — the controller does not track or dispose relations for you.

Scoped validation #

For a large aggregate, pass scopeOf so each write re-validates only the subtree it falls under instead of the whole draft. keyed_form_gen wires this up for you — InvoiceForm.validateData honours the scope and InvoiceForm.scopeOf (which delegates to rowScopeOf) is a sensible default:

KeyedFormController<InvoiceForm>(
  initialValue: const InvoiceForm(),
  resolver: InvoiceForm.validateData,
  scopeOf: InvoiceForm.scopeOf, // or your own: (key) => key.prefix(2)
);

validateScopes re-validates and force-reveals a set of subtrees at once — the "validate every dirty row before saving" operation — and returns which of them still fail. Cross-field .refine(...) rules that sit above the written scope only re-run on a full validate() (or validateScopes covering them), so keep calling validate() on submit.

Scope #

Deliberately out of scope: widgets (that is keyed_form_flutter), schema/validation DSL (that is keyed_form_schema), and any serialization format for the draft itself.

1
likes
160
points
--
downloads

Documentation

API reference

Publisher

verified publisherv4g.space

Weekly Downloads

A form-state controller for immutable aggregates, built on keyed_form_core: holds the working draft, FieldKey-addressed validation errors and touched/dirty/submit state.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

keyed_form_core, listen, meta

More

Packages that depend on keyed_form