Subforms

Wizards

One subform per step makes per-step validation one line, and the final submit re-checks every step the user walked over.

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 step, so a step the user walked back over is checked again.

wizard.dartidle
Source3 files
wizard_steps.dart
/// One step of the wizard: an ordinary form controller with a title.
abstract class WizardStep extends AdvancedFormController {
  String get title;
}

final _emailPattern = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');

class AccountStep extends WizardStep {
  AccountStep() {
    registerFields([email, password]);
  }

  @override
  String get title => 'Account';

  final email = AdvancedTextFieldController(
    validator: (value) =>
        _emailPattern.hasMatch(value) ? null : 'Enter an email',
  );
  final password = AdvancedTextFieldController(
    validator: atLeastLength(8, 'At least 8 characters'),
  );
}

class AddressStep extends WizardStep {
  AddressStep() {
    registerFields([country, city]);
    // Picking another country clears the city — a relation, not a rule.
    addRelation(country, (value) => value, (_) => city.reset());
  }

  @override
  String get title => 'Address';

  final country = AdvancedSingleSelectFieldController<String, String>(
    initialValue: null,
    options: const ['Poland', 'Germany', 'Spain'],
    validator: notNull('Pick a country'),
  );
  final city = AdvancedTextFieldController(validator: filled('Enter a city'));
}

class ConfirmStep extends WizardStep {
  ConfirmStep() {
    registerFields([terms]);
  }

  @override
  String get title => 'Confirm';

  final terms = AdvancedBooleanFieldController<String>(
    validator: mustBeTrue('You have to accept the terms'),
  );
}
DocsActions, DocsDropdownField, DocsSubmitButton, DocsSwitchField and DocsTextField are shorthands these docs define, not part of the package. See Rendering fields for the widget code an app writes.

Three decisions in that code are worth keeping:

  • Create the wizard controller above the pages. A controller built inside a step page dies with it, taking that step's values and errors along. With routes, provide it above the Navigator or hand it to each route with ChangeNotifierProvider.value.
  • Escalate the failed step, not the whole form. After a failed next(), the step gets ValidationMode.onUserInteraction so it corrects itself as the user types. Because a subform's own mode wins over the parent's, the steps ahead stay in manual and never flag an untouched field.
  • A conditional step stays attached with setValidationEnabled(false). The example app's Step Form screen has an invoice step that a switch on the address step adds or drops through addRelation. Switched off, the step leaves the flow and validate(), canSubmit and validating, so one flag is the whole condition — and its values survive in case the user flips the switch back.