Available modes
The four modes that ship with the package, and the three rules and one table they come from — validate on submit, on every keystroke, when a field is left, or live once it was first left.
A validator is a function: value in, error out, null means valid. When it runs is decided by the form's
ValidationMode, and the whole trigger behaviour fits in three rules.
- The mode decides which events make a field validate itself. Set it once on the form; it reaches every field and subform, including ones registered or attached later.
- A field the user has never edited validates nothing on its own, in every mode.
validate()is what checks those, so a prefilled form does not greet the user with errors. - A round runs the sync validator first, and the async validator only if sync passed.
validate() obeys none of them: it ignores the mode, never changes it, and runs every field — touched or not.
The four modes
ValidationMode | What the user sees |
|---|---|
manual (default) | Nothing validates until validate() is called — usually on submit. Editing a field still clears the error that described its old value |
onUserInteraction | Every edit validates the field being edited; the async check waits out its debounce |
onUnfocus | Leaving a field the user edited validates it. Tabbing through an untouched field costs nothing; a debounced async check is flushed on the way out |
afterFirstUnfocus | Quiet while the user types, until the field is first left after an edit. From then on every edit validates it, so an error disappears the moment the value is fixed |
The gate, in full — the row is the mode, the column is the event:
| mode | value changed | focus left | a dependency changed |
|---|---|---|---|
manual | — | — | — |
onUserInteraction | validate | — (the edit already did) | sync only |
onUnfocus | — (still typing) | validate | sync only |
afterFirstUnfocus, not yet left | — (still typing) | validate | sync only |
afterFirstUnfocus, left once | validate | — (the edit already did) | sync only |
afterFirstUnfocus is the onUnfocus row until the user first leaves the field after editing it, and the
onUserInteraction row from then on. Only that unfocus flips it: a validate() from a submit button does not, and
reset() flips it back. A field the user tabbed through without editing has not been "left" in this sense, so it still
waits. Like onUnfocus, it needs the widget to bind field.focusNode (see Configuring the mode).
"A dependency changed" is what subscribeToFields and validateAll produce; it re-runs the sync
validator only, because this field's own value did not change and its async verdict still stands.
A mode of your own
The four modes are constants of ValidationMode. A rule none of them states is a class that extends
CustomValidationMode and overrides one method: it sees which event is happening and what happened to the field
before, and answers how much to run. Everything else on this page still holds for it, the untouched-field rule
included. Your own validation mode has the whole story.
Try it
Switch the mode while you type. The form is the same; only the trigger changes.
Source2 files
class ProfileFormController extends AdvancedFormController {
ProfileFormController() {
registerFields([username, website]);
}
final username = AdvancedTextFieldController(
validator:
filled('Username is required') &
atLeastLength(3, 'At least 3 characters'),
);
final website = AdvancedTextFieldController(
validator: (value) => value.isEmpty || value.startsWith('https://')
? null
: 'Must start with https://',
);
}DocsActions, DocsHint, DocsLinkBanner, DocsModePicker, DocsSubmitButton and DocsTextField are shorthands these docs define, not part of the package. See Rendering fields for the widget code an app writes.