diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aba5f2..6cb288c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/subforms/index.mdx b/docs/subforms/index.mdx index c20c411..2e64c84 100644 --- a/docs/subforms/index.mdx +++ b/docs/subforms/index.mdx @@ -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. diff --git a/docs/subforms/patterns.mdx b/docs/subforms/patterns.mdx index 41ab710..f135081 100644 --- a/docs/subforms/patterns.mdx +++ b/docs/subforms/patterns.mdx @@ -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. diff --git a/docs/subforms/wizard.mdx b/docs/subforms/wizard.mdx index f704a5e..18eacf6 100644 --- a/docs/subforms/wizard.mdx +++ b/docs/subforms/wizard.mdx @@ -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. diff --git a/lib/src/form/advanced_form_controller.dart b/lib/src/form/advanced_form_controller.dart index 883b247..465a238 100644 --- a/lib/src/form/advanced_form_controller.dart +++ b/lib/src/form/advanced_form_controller.dart @@ -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 — @@ -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(); } } } diff --git a/skills/advanced_forms-build-forms/SKILL.md b/skills/advanced_forms-build-forms/SKILL.md index 03da8bf..cf0ba6b 100644 --- a/skills/advanced_forms-build-forms/SKILL.md +++ b/skills/advanced_forms-build-forms/SKILL.md @@ -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. diff --git a/test/src/form/form_controller_test.dart b/test/src/form/form_controller_test.dart index 8356c42..bf5ac52 100644 --- a/test/src/form/form_controller_test.dart +++ b/test/src/form/form_controller_test.dart @@ -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(form);