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.
A section behind a switch
Invoice details behind an "I need an invoice" checkbox, a shipping address unless "same as billing" is on: hand the
condition to addSubform.
CheckoutFormController() {
registerFields([email, needsInvoice]);
addSubform(invoice, enabled: () => needsInvoice.fieldValue);
}The closure is evaluated at addSubform and again whenever a value anywhere in the form changes. While it is true the
section is attached; when it turns false the section is detached, exactly as removeSubform would: its fields leave
validate(), canSubmit, getFieldValues() and allFields, so the payload you build from the form has no invoice
in it. The form owns the section either way, so the values stay in its fields and are there again when the
checkbox goes back on, while its errors are cleared on the way out — and the section is disposed with the form.
The closure can be any boolean expression over the fields' values. Because the form re-evaluates it after resetAll()
too, a reset checkbox takes the section with it.
A condition that depends on something outside the form — a service, a route argument — is not re-evaluated for that;
attach and detach by hand then.
Which to use. enabled: when the decision is a function of the form's own values; addSubform and
removeSubform by hand when it comes from outside the form. The two do not fight, the last call by hand wins: a manual
removeSubform detaches the section and drops the condition, so the section stays out until the next addSubform;
a plain addSubform attaches it for good; addSubform with a new enabled: replaces the condition.
Or keep it attached and switch validation off
Detached, the section is out of reach of resetAll, markReadOnly, clearErrors and setValidationMode, and its
values are not part of the form's. When you want those to keep reaching a section that merely does not apply right
now, keep it attached and use setValidationEnabled(false) instead:
- its errors are cleared and its fields leave
canSubmit,validating,hasFailedValidationandvalidationErrors; - its
validate()returnstruewithout running; - everything else —
resetAll,markReadOnly,clearErrors, disposal — still reaches it, and its values stay ingetFieldValues(); - switching back on re-runs the sync validators at once.
addSubform(invoice);
addRelation(needsInvoice, (on) => on, invoice.setValidationEnabled);
invoice.setValidationEnabled(needsInvoice.fieldValue); // relations fire on change onlyresetAll() does not touch validationEnabled, so seed it again wherever you reset.
Swapping one section for another
A type selector that swaps the active section — a person or a company — is removeSubform of one and
addSubform of the other, reacting to the selector field with addRelation. Only the attached section takes part in
validation; the detached one keeps its values in case the user switches back, and both are disposed with the parent.
addRelation(type, (t) => t, (t) {
if (t == CustomerType.company) {
removeSubform(person);
addSubform(company);
} else {
removeSubform(company);
addSubform(person);
}
});The example app's Complex Form screen is this pattern; Delivery List Form is the dynamic list; Step Form is the wizard with a conditional step. See the example app.
Things that throw
addSubform and removeSubform throw a descriptive StateError when called on a disposed form, and addSubform
also when handed a disposed subform. A disposed controller cannot be reused — build a fresh one per appearance, or keep
the section attached and switch it off. Lifecycle and ownership has the full table.