Skip to content

MudForm: Base IsValid on required fields having a value, not being touched#13423

Merged
danielchalmers merged 10 commits into
MudBlazor:devfrom
danielchalmers:fix/mudform-isvalid-required-hasvalue
Jul 6, 2026
Merged

MudForm: Base IsValid on required fields having a value, not being touched#13423
danielchalmers merged 10 commits into
MudBlazor:devfrom
danielchalmers:fix/mudform-isvalid-required-hasvalue

Conversation

@danielchalmers

@danielchalmers danielchalmers commented Jul 3, 2026

Copy link
Copy Markdown
Member

MudForm decided validity from requiredAllTouched, using Touched as a proxy for "required field satisfied".

Fix: base validity on whether each required field holds a value.

  • IFormComponent.HasValue() — a default interface method (returns Touched, so existing external implementers are unaffected), overridden on MudFormComponent; empty strings and empty collections count as no value.
  • MudForm uses noErrors && required.All(x => x.HasValue()) in both OnEvaluateForm and first-render init.

Behavior notes for upgraders:

  • A pre-populated valid form is now valid on load; an empty required field is still invalid (no error shown until touched); an unselected required MudRadioGroup is now correctly invalid.
  • A pre-populated value that would fail a custom Validation delegate reads as valid until it is validated (the delegate runs on interaction), and a debounced field with an uncommitted value is treated as having no value until it commits.

Related issues

@mudbot mudbot Bot added bug Unexpected behavior or functionality not working as intended regression Previously worked and now doesn't labels Jul 3, 2026
@danielchalmers
danielchalmers requested a review from Copilot July 3, 2026 18:32

This comment was marked as outdated.

@danielchalmers

This comment was marked as outdated.

This comment was marked as outdated.

@danielchalmers danielchalmers added enhancement Adds a new feature or enhances existing functionality (not fixing a defect) in the main library API change Modifies the public API surface in a non-breaking way (ex: adds a new property) and removed regression Previously worked and now doesn't bug Unexpected behavior or functionality not working as intended labels Jul 3, 2026
@danielchalmers

danielchalmers commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Reproduced the actual consumer snippets in a live browser harness:

  • Required int=0 → valid; int? null → invalid; int?=0 → valid.
  • Select / MultiSelect / Autocomplete / Radio / DatePicker / TimePicker: preset → valid, empty → invalid.
  • CheckBox/Switch unchecked → invalid (the BoolValue == true override holds — "must agree" still works); checked → valid.
  • No-required-fields form → valid; ResetAsync clears a pre-filled required field → invalid.
  • Exact #13421 register form (username + email/EmailAddressAttribute + radio + checkbox, all pre-filled): IsValid=true on load, Register button enabled — the regression is fixed.
  • Failing-validator probe: a required field pre-filled "ab" with a custom validator requiring length ≥ 5 reads valid on load, but Submit → ValidateAsync() flips IsValid=false, disables the button, and shows the error. The valid-on-load state is transient and self-corrects on the standard submit path.

Two behaviours worth noting so they aren't misread as new bugs:

  1. Validators run lazily. A pre-populated field whose value would fail a custom/DataAnnotation validator still reads valid on load — validators run on interaction or ValidateAsync(), exactly as before. A submit handler that calls ValidateAsync() (or MudForm's EditForm integration) catches it; Disabled="@(!IsValid)" alone was never a full guard.

  2. Nested forms. A nested MudForm whose parent binds IsValid may report stale on first render until the child form's validity propagates; it corrects on the first field interaction. This is a pre-existing ChildForms aggregation quirk, orthogonal to this change, and is tracked separately.

This comment was marked as outdated.

…t (review)

Avoids the Cast<object?>().Any() enumerator allocation for the common ICollection case; the foreach fallback disposes the enumerator for other IEnumerable types.
… a fast-path

The ICollection/foreach form added a coverage gap and a Sonar S1751 'loop only runs once' smell for a negligible gain on a branch MudSelect already overrides. Cast().Any() is a single covered statement.
Exercises the => Touched fallback that external implementers without a HasValue override rely on.
@danielchalmers
danielchalmers merged commit 0fee551 into MudBlazor:dev Jul 6, 2026
10 checks passed
This was referenced Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API change Modifies the public API surface in a non-breaking way (ex: adds a new property) breaking change This change will require consumer code updates (ex: removes/changes an API) bug Unexpected behavior or functionality not working as intended enhancement Adds a new feature or enhances existing functionality (not fixing a defect) in the main library regression Previously worked and now doesn't

Projects

None yet

2 participants