Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased

* A section detached by its `enabled:` condition has its errors cleared, so it does not come back red.

## 0.2.2

* Conditional sections in one line: `addSubform(invoice, enabled: () => needsInvoice.fieldValue)` attaches and detaches the section as the checkbox flips, and its values survive.
Expand Down
3 changes: 2 additions & 1 deletion docs/subforms/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ class _GuestCard extends StatelessWidget {
- **`addSubform(form)`** attaches. A no-op if already attached. The parent applies its validation mode to the subform
at once — unless the subform has a mode of its own.
- **`addSubform(form, enabled: () => …)`** attaches a conditional section: the closure decides, now and after every
value change in the form, whether the section is attached. Detached, it keeps its values and its owner. See
value change in the form, whether the section is attached. Detached, it keeps its values and its owner, and its
errors are cleared. See
[Patterns](./patterns.mdx#a-section-behind-a-switch).
- **`removeSubform(form)`** detaches: the subform stops validating, notifying and counting towards the parent's state,
and can be re-attached later. It does **not** dispose it.
Expand Down
2 changes: 1 addition & 1 deletion docs/subforms/patterns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The closure is evaluated at `addSubform` and again whenever a value anywhere in
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 — and the section is disposed with the form.
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.
Expand Down
2 changes: 1 addition & 1 deletion docs/subforms/wizard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,4 @@ Three decisions in that code are worth keeping:
- **A conditional step comes and goes with its switch.** The example app's *Step Form* screen attaches its invoice step
with `addSubform(invoice, enabled: () => address.needsInvoice.fieldValue)`. Detached, the step leaves the flow *and*
`validate()`, `canSubmit` and `validating`, so membership is the whole condition — and the wizard still owns it, so
its values survive in case the user flips the switch back.
its values survive in case the user flips the switch back, with its errors cleared.
6 changes: 5 additions & 1 deletion lib/src/form/advanced_form_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ class AdvancedFormController
/// false the section is detached, as with [removeSubform]: it leaves
/// `validate`, `canSubmit`, `getFieldValues` and every other broadcast. This
/// form owns the section either way, so its values stay in its fields and
/// are there again when it comes back — and it is disposed with this form.
/// are there again when it comes back, while its errors are cleared on the
/// way out — and it is disposed with this form.
///
/// Which to use: [enabled] when the decision is a function of the form's
/// own values; the two calls by hand when it comes from outside the form —
Expand Down Expand Up @@ -293,7 +294,10 @@ class AdvancedFormController
if (next) {
_attach(form);
} else {
// The section left because its switch went off: keep the values for
// its return, but not the red marks of a section that no longer applies.
_detach(form);
form.clearErrors();
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions skills/advanced_forms-build-forms/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,8 +758,8 @@ class CheckoutFormController extends AdvancedFormController {
The closure runs at `addSubform` and again after every value change anywhere in this form's
tree. True → the section is attached; false → detached, exactly like `removeSubform`: out of
`validate`, `canSubmit`, `getFieldValues`, `allFields` and every broadcast. **The form owns
the section either way**, so its values stay in its fields and are back when the switch flips,
and it is disposed with the form. The closure can be any boolean expression over the fields'
the section either way**, so its values stay in its fields and are back when the switch flips
(its errors are cleared on the way out), and it is disposed with the form. The closure can be any boolean expression over the fields'
values. `resetAll()` re-triggers it, so a reset switch takes the section with it. Do **not** write `addRelation` + `addSubform`/`removeSubform`
for this. A condition on something *outside* the form (a service, a route argument) is not
re-evaluated for that — attach and detach by hand then.
Expand Down
11 changes: 11 additions & 0 deletions test/src/form/form_controller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,17 @@ void main() {
expect(subformField.fieldValue, 42);
});

test('a section detached by its condition has its errors cleared', () {
needsInvoice.setValue(true);
form.addSubform(subform, enabled: () => needsInvoice.fieldValue);
subformField.setError(_Error2.malformed);

needsInvoice.setValue(false);

expect(subformField.value.error, isNull);
expect(subformField.fieldValue, _initialValue2);
});

test('a change that leaves the result the same does nothing', () {
form.addSubform(subform, enabled: () => needsInvoice.fieldValue);
final emissions = _record<AdvancedFormState>(form);
Expand Down
Loading