Validation

Cross-field rules

subscribeToFields re-runs a field's sync validator when the fields it depends on change — repeat-password in two lines.

Two things look alike and are not: a rule on field B that reads field A ("passwords must match"), and a value of field B computed from field A ("total is quantity times price"). This page is the first; Relations is the second.

subscribeToFields re-runs this field's sync validator whenever the fields it depends on change value. Mismatch the two fields below, then fix the first one: the second field's error clears itself.

password_form.dartidle
Source2 files
password_form_controller.dart
class PasswordFormController extends AdvancedFormController {
  PasswordFormController()
    : super(validationMode: ValidationMode.onUserInteraction) {
    registerFields([password, repeatPassword]);
  }

  final password = AdvancedTextFieldController(
    validator: atLeastLength(8, 'Password is too short'),
  );

  // `late`, because the validator reads a sibling.
  late final repeatPassword = AdvancedTextFieldController(
    validator: (value) =>
        value == password.fieldValue ? null : 'Passwords do not match',
  )..subscribeToFields([password]);
}
DocsTextField is a shorthand these docs define, not part of the package. See Rendering fields for the widget code an app writes.

What it does, precisely

  • It re-runs this field's sync validator. It does not copy or derive values, and it does not re-run the async validator — this field's own value did not change, so its verdict still stands.
  • It obeys the gate: nothing happens while this field is in ValidationMode.manual, and nothing on a field the user has never edited. Under manual the mismatch shows up on submit instead.
  • It fires when a watched value changes, never on a status-only change of the sibling, so it cannot loop.
  • A second call replaces the previous subscription; the subscription is dropped on dispose.
  • A code pushed with setError gives way to whatever the validator now returns, like any other re-run.

Two fields watching each other

One rule spanning a pair — "book at least one person" across adults and children — needs the rule in both validators and a subscription in both directions. Wire the subscriptions in the constructor body, after registerFields, never in the field initialisers: a ..subscribeToFields([sibling]) cascade inside a late final initialiser evaluates the sibling eagerly, and a mutual pair fails at construction.

BookingFormController() {
  registerFields([adults, children]);
  adults.subscribeToFields([children]);
  children.subscribeToFields([adults]);
}

late final adults = AdvancedTextFieldController<MyError>(
  validator: (_) => _somebodyTravels() ? null : MyError.nobodyTravels,
);
late final children = AdvancedTextFieldController<MyError>(
  validator: (_) => _somebodyTravels() ? null : MyError.nobodyTravels,
);

bool _somebodyTravels() =>
    (int.tryParse(adults.fieldValue) ?? 0) +
        (int.tryParse(children.fieldValue) ?? 0) >
    0;

An error only ever appears on a field whose own validator returns it, which is why the rule is duplicated; the subscriptions are only what re-runs it.

Everything depends on everything

Pass validateAll: true to the superclass and any field's value change re-runs the sync validator on every autovalidating field in the tree — blunt but correct when most fields cross-validate. Like subscribeToFields, it re-runs sync validators only, so typing in one field never starts a network check on another. The same broadcast is available on demand as form.revalidateSync(), for something other than a field value that changes what "valid" means — a locale switch, a feature flag.

On this page