Subforms

Attach nested form controllers so their fields join the parent's validate, reset, and error handling — including wizards, one subform per step.

addSubform attaches another AdvancedFormController as a child, and its fields join the parent's validate, markReadOnly, setValidationEnabled, and the other broadcast operations. Use it for sections that appear dynamically, or to split a large form into readable pieces:

class BaseFormController extends AdvancedFormController {
  BaseFormController() {
    registerFields([field]);
  }

  final field = AdvancedTextFieldController();
  final subform = SubformController();

  void extendForm() => addSubform(subform);
}

void removeSubform(form) detaches a subform: it stops validating, notifying and counting towards the parent's state, and can be re-attached later with addSubform. It does not dispose it — the parent owns every subform it was ever given and disposes them all in its own dispose(), the same way it owns registered fields. So don't dispose subforms yourself; if you do, the parent skips them rather than disposing them twice. Calling addSubform with — or on — a disposed controller throws a descriptive StateError rather than crashing later, and so do registerFields, setValidationEnabled and removeSubform on one.

Working examples: DeliveryListFormScreen (a dynamic list), ComplexFormScreen (swapping one subform for another) and StepFormScreen (a wizard: one subform per step).

A wizard, validated step by step

One subform per step is what makes per-step validation one line — await step.validate() reaches that step's fields and nothing else, so a field on a later step is never flagged before the user gets there. The final submit calls the parent's validate(), which walks every attached subform, so a step the user walked back over is checked again:

Future<bool> next() async {
  final step = currentStep;
  if (!await step.validate()) {
    // The step has shown its errors once — let it correct itself as the user
    // types. A subform's own mode wins over the parent's, so the steps ahead
    // stay quiet.
    step.setValidationMode(ValidationMode.onUserInteraction);
    return false;
  }
  _goTo(currentIndex + 1);
  return true;
}

Which step is on screen is navigation, not form state: keep it on your own controller, and create that controller above the pages — a wizard controller built inside a step page dies with it, taking that step's values and errors along. Working example: StepFormScreen (example/lib/screens/step_form.dart).

On this page