Validation

Relations between fields

addRelation derives one field's value from another — a running total, a mirrored field, a selection cleared when its parent changes.

For "when B changes, set A" — recompute a total, mirror one field into another, clear a dependent selection — use the form's addRelation. Type a quantity below; the total is a read-only field the relation writes.

order_total.dartidle
Source2 files
order_total_controller.dart
class OrderTotalController extends AdvancedFormController {
  OrderTotalController() {
    registerFields([quantity, total]);
    // `select` picks the part of the source to watch; `onChange` fires only
    // when that part changes. Nothing to clean up: the form removes the
    // listener in its own dispose().
    addRelation(
      quantity,
      (value) => int.tryParse(value) ?? 0,
      (count) => total.setValue(count * unitPrice, force: true),
    );
  }

  static const unitPrice = 12.5;

  final quantity = AdvancedTextFieldController(
    initialValue: '1',
    validator: positiveInteger('Enter a whole number above zero'),
  );

  // A display field: read-only, so only the relation writes it.
  final total = AdvancedFieldController<double, String>(
    initialValue: unitPrice,
    name: 'total',
  )..markReadOnly();
}
DocsTextField is a shorthand these docs define, not part of the package. See Rendering fields for the widget code an app writes.

addRelation(source, select, onChange) calls onChange whenever the part of source's value picked by select changes, compared with ==. Status-only changes never fire it, so unlike a raw addListener pair a mutual relation cannot loop. Three details matter in practice:

  • It fires on change, not at registration. Seed the target with the derived value as its initialValue, as total does above, or call onChange once by hand right after addRelation.
  • A derived field is usually read-only, and setValue on a read-only field is a no-op — so the relation passes force: true.
  • Destructive relations are safe. addRelation(country, (c) => c, (_) => city.reset()) clears the city when the country changes and nothing else can wipe the user's choice; the wizard does exactly that.

Attaching and detaching subforms, or calling setValidationEnabled, from onChange is supported too — that is how a switch on one page adds or drops another page of a wizard.

Which one do I want?

I need to…Use
re-check B's rule when A changesB.subscribeToFields([A])
re-check every rule when anything changesvalidateAll: true
re-check every rule because something outside the form changedform.revalidateSync()
set B's value when A changesaddRelation(A, select, (a) => B.setValue(…))
forget B's async verdict when A changesaddRelation(A, select, (_) => B.clearErrors())
react to A outside the form (analytics, a service)A.addListener(…), comparing values yourself

On this page