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.
Source3 files
/// 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
Navigatoror hand it to each route withChangeNotifierProvider.value. - Escalate the failed step, not the whole form. After a failed
next(), the step getsValidationMode.onUserInteractionso it corrects itself as the user types. Because a subform's own mode wins over the parent's, the steps ahead stay inmanualand 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 throughaddRelation. Switched off, the step leaves the flow andvalidate(),canSubmitandvalidating, so one flag is the whole condition — and its values survive in case the user flips the switch back.
Attaching subforms
addSubform makes another form controller part of this one — its fields join validate, reset and every other broadcast. A dynamic list is the simplest case.
Subform patterns
Detach a section or switch its validation off? Swap one section for another? The trade-offs, and where the example app shows each.