Skip to content

A conditional section in one line - #91

Merged
KamilSztandur merged 4 commits into
mainfrom
conditional-subforms
Sep 23, 2026
Merged

KamilSztandur merged 4 commits into
mainfrom
conditional-subforms

Conversation

@KamilSztandur

@KamilSztandur KamilSztandur commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A section that applies only sometimes, like invoice details behind an "I need an invoice" switch, takes a relation and two calls in every form:

addSubform(invoice);
addRelation(needsInvoice, (on) => on, (on) => on ? addSubform(invoice) : removeSubform(invoice));
if (!needsInvoice.fieldValue) removeSubform(invoice); // relations fire on change only

The seeding line is easy to forget, and the whole thing says nothing about what it is for. Piotr suggested that addSubform should just take the condition and do the attaching and detaching itself.

Solution

addSubform(invoice,  enabled: () => needsInvoice.fieldValue);
addSubform(shipping, enabled: () => !sameAsBilling.fieldValue);
addSubform(company,  enabled: () => type.fieldValue == CustomerType.company);

One optional parameter on a method everyone already uses. The form evaluates the closure when the section is added and again after every value change anywhere in its tree. While it is true the section is attached. When it turns false the section is detached, exactly as removeSubform would do: its fields leave validate(), canSubmit, getFieldValues() and allFields, so a payload built from the form has no invoice in it. 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. A reset of the checkbox takes the section with it on its own. removeSubform drops the condition.

Cost: one closure call and a bool comparison per conditional section on each value change, next to the pass over all fields the form already does for wasModified.

Limit, written into the docs: a condition on something outside the form is not re-evaluated for that change. Attaching by hand still covers it.

In this PR

  • enabled on addSubform, re-evaluated from the existing value-changed handler; attach and detach share the code with addSubform and removeSubform.
  • Tests: detached but owned, attached at once when true, follows the fields it reads, values survive a detach, quiet when the result does not change, reacts to a change inside another subform, out of validate() while detached, parent mode reaches a late attach, resetAll() re-syncs, removeSubform drops the condition, re-attach and replace cases, outside field not followed.
  • Example app: the wizard's invoice step uses it; the navigation follows the attached steps.
  • Docs (subform patterns, subform index, wizard, relations table), SKILL.md and a changelog entry. The switch-validation-off pattern stays documented as the alternative for when resetAll and friends must keep reaching a hidden section.

flutter analyze clean in the package and the example, all 273 tests pass.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Docs preview: https://advanced-forms-4zlmbh1dh-leancode.vercel.app

Built from 1cf2ddf; the landing page is at /, the docs under /docs.

@mateusz-pietras

Copy link
Copy Markdown
Member

IDK, as a user I think I would feel lost in this, when to use removeSubform, when to make it conditional... What about a separate method for that, leaving add/remove as an advanced feature, while idk attachSubform(form, condition) would stay a default way? I dont like either of these options tbh

@KamilSztandur KamilSztandur mentioned this pull request Sep 21, 2026
KamilSztandur and others added 2 commits September 21, 2026 23:02
The closure is evaluated on attach and after every value change in the
form; its result switches the section's validation on and off. Replaces
the addRelation + setValidationEnabled + manual seed pattern, and a
resetAll() brings the section back in step by itself. Piotr Denert's
idea. Example wizard, docs and skill updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Kamil's call: a section whose condition is false leaves the form, so
the payload built from the form has none of its fields. The form still
owns it, so its values survive and come back when the condition flips.
Example wizard, docs and skill follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The last call by hand wins: removeSubform detaches and drops the
condition, a plain addSubform attaches for good. In the Dart docs, the
patterns page and the skill.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@KamilSztandur

Copy link
Copy Markdown
Collaborator Author

@mateusz-pietras After reconsideration I agree that's API doubling, but I think it's in good faith.

Adding subform with "enabled by" hook, then removing it with "removeSubform" will simply deactivate that hook.

To make some order of this "chaos" I expanded Dart docs for this "enabledBy" parameter, explaining that it's convenient shortcut for most simple use cases

Let me know what you think:)

Comment thread docs/subforms/patterns.mdx Outdated
Comment thread lib/src/form/advanced_form_controller.dart Outdated
Comment thread lib/src/form/advanced_form_controller.dart Outdated
Comment thread skills/advanced_forms-build-forms/SKILL.md Outdated
Piotruś's four comments on #91: the condition is a typedef of a record
and the loop destructures the entry; the docs and the skill say the
closure is any boolean expression over the fields' values instead of
listing examples.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@mateusz-pietras mateusz-pietras left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@KamilSztandur
KamilSztandur merged commit fc3070b into main Sep 23, 2026
5 checks passed
@KamilSztandur
KamilSztandur deleted the conditional-subforms branch September 23, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants