Repository navigation
Add a typed builder method for the native required opt-in instead of a magic string #204
Description
Problem / motivation
#193 made .WithAttribute("Required", true) the supported way to opt a collection item field back into
MudBlazor's native required semantics — the asterisk and the HTML5 required attribute — after
.Required(...) stopped implying them. The README documents that string as the public API.
A raw string is a poor public API here, for three separate reasons — all of which fail silently:
1. The key is case-sensitive, and the bag mixes two conventions. AdditionalAttributes carries both
MudBlazor PascalCase keys and lowercase HTML-ish ones:
| Convention | Keys in use |
|---|---|
| PascalCase (MudBlazor parameters) | "Variant", "ShrinkLabel", "Adornment", "AdornmentIcon", "AdornmentColor", "Required" |
| lowercase (HTML-ish, written by the attribute/auto builder extensions) | "min", "max", "rows", "step", "options" |
A developer who has met the lowercase family writes .WithAttribute("required", true) and gets
nothing: no asterisk, no exception, no warning. GetItemFieldAttribute falls back to false on a
miss, as it must.
2. The value is untyped. .WithAttribute("Required", "true") — a string, not a bool — also silently
does nothing, because the resolver requires value is bool.
3. Every sibling has a typed method; this one doesn't. FieldBuilderExtensions.cs declares
WithVariant (line 133), WithShrinkLabel (line 175) and WithAdornment (line 199). Required is the
only MudBlazor-facing presentation attribute whose documented public API is a raw string.
Two further sharp edges the API should surface rather than hide:
- The opt-in is collection-only. No component-path renderer reads a
"Required"key, so the same
call on an ordinary field does nothing. (fix(mudblazor): stop emitting the HTML5 Required attribute on collection item fields (#190) #193 closed one divergence and opened this one; it is listed
inRenderPipelineParityTests' divergence block.) - The opt-in is inert on boolean item fields, because
RenderBooleanFieldbypasses
AddCommonFieldAttributesentirely.
Proposed solution
Add a typed builder method — e.g. .WithNativeRequired(bool enabled = true) — alongside WithVariant,
WithShrinkLabel and WithAdornment, that writes the canonical "Required" key. Document it as the
supported opt-in and demote the raw string to an implementation detail.
Naming matters more than usual here: it must not read as a synonym for .Required(...), which is the
validation API. Something that says "MudBlazor's native required decoration", not "this field is
required".
The method should also work on both render paths, or say plainly that it does not.
Alternatives considered
- Keep the raw string, document it harder. Rejected — documentation does not fix a silent
case-sensitivity failure, and the README already documents it. - Make the lookup case-insensitive. Fixes the typo class but not discoverability or type safety, and
changing the shared attribute bag's comparer affects every consumer ("min"vs"Min"), which is a
much larger blast radius than the problem justifies. - Warn when an unrecognised attribute key is set. A genuinely useful diagnostic in its own right —
the library already has a diagnostics channel (ShrinkLabelDiagnostic) — but it needs a whitelist of
valid keys to avoid false positives on deliberate passthrough attributes. Worth its own issue;
complementary rather than an alternative.
Area
FormCraft.ForMudBlazor — builder extensions / public API
Follow-up from #193. Related: #190, #184, #177
🧠 Brainstorm
Problem / context
The project's conventions are explicit that the fluent API is the surface: Add* adds, With*
configures, no magic strings for property names, strong typing throughout. .WithAttribute("Required", true) violates the spirit of all three, and it got there by accident — #193 needed an escape hatch and
the attribute bag was the cheapest place to put one.
The failure mode is the worst kind: the developer's code compiles, runs, and does nothing, with no
signal anywhere. Compare .WithAdornment(...), which is impossible to misspell.
Approaches
A. Typed method writing the canonical key.
Pros: matches every sibling; discoverable via IntelliSense; type-safe; the string becomes internal.
Cons: one more public API to name well and support; does not by itself fix the raw-string path for
anyone already using it (which still works, and should).
B. Case-insensitive attribute lookup.
Pros: fixes the specific typo. Cons: doesn't help discoverability or typing; changing the bag's
comparer touches every attribute consumer; two spellings both "working" is its own confusion.
C. Diagnostic warning on unknown attribute keys.
Pros: catches this whole class of silent failure, not just Required. Cons: needs a curated key
whitelist or it fires on legitimate passthrough attributes; larger design question.
Recommendation
A, and file C separately — the diagnostic is valuable but is a different feature with a
different risk profile. Do not take B.
While implementing A, decide explicitly whether the opt-in should work on the component path too. It
probably should — an escape hatch that exists on one path is the same divergence class this library
keeps filing bugs about. But that is a behaviour change and deserves to be a deliberate line in the
release notes, not a side effect.
📋 Spec
Goal
Opting into MudBlazor's native required decoration is a discoverable, type-safe, misspelling-proof
builder call, consistent with WithVariant / WithShrinkLabel / WithAdornment.
Scope
- Add a typed extension on
FieldBuilder<TModel, TValue>writing the canonical"Required"key. - Update the README's Collection item fields emit the HTML5 Required attribute, contradicting the project's validation convention #190 release note to document the typed method as the supported opt-in.
- Decide, implement and test whether the opt-in applies to the component path as well as the collection
path — and state the answer in the parity test's divergence list either way. - Cover the boolean-item-field inertness explicitly (test or fix; not silence).
Non-goals
- Changing what
.Required(...)does — it remains validation-only (Collection item fields emit the HTML5 Required attribute, contradicting the project's validation convention #190). - Making the attribute bag case-insensitive.
- The general "unknown attribute key" diagnostic — separate issue.
Behaviour
flowchart TD
A[".WithNativeRequired()"] --> B["AdditionalAttributes[\"Required\"] = true"]
B --> C{render path}
C -->|collection item| D["Required=true → asterisk + HTML5 required"]
C -->|ordinary field| E["decide: honour it, or document as collection-only"]
F[".WithAttribute(\"required\", true)<br/>lowercase typo"] --> G["silently ignored — the bug this closes"]
Key files
FormCraft.ForMudBlazor/Extensions/FieldBuilderExtensions.cs— whereWithVariant(133),
WithShrinkLabel(175) andWithAdornment(199) live; the new method joins them.FormCraft.ForMudBlazor/Features/CollectionField/CollectionFieldComponent.razor.cs—
GetItemFieldAttribute(field, "Required", false), the current reader.FormCraft.ForMudBlazor.UnitTests/Fields/CollectionRequiredTests.cs— existing opt-in coverage.README.md— the## 🎉 Unreleasednote that currently documents the raw string.
Validation rules
- The typed method sets the same key the renderer reads; a field configured with it renders
Required="true"and themud-input-requiredclass. - It is available on text, numeric and date item fields (the three renderers fed by the shared helper).
.Required("…")alone still renders no HTML5requiredattribute — Collection item fields emit the HTML5 Required attribute, contradicting the project's validation convention #190's guarantee is untouched.- The existing raw-string form keeps working (no breaking change).
- Whatever is decided for the component path is asserted by a test, not left implicit.
Edge cases
- Naming collision with
.Required(...)— the two must not read as synonyms; this is the main
design risk. - Generic constraint:
WithAdornmentis declared onFieldBuilder<TModel, string>only, which is
why numeric fields configure adornments through raw attributes. Don't repeat that — required
decoration is meaningful for numeric and date fields too. - Boolean item fields take no shared attributes at all; the method must be tested as inert there or
made to work. .WithAttribute("Required", "true")(string value) stays inert — the typed method is the fix.
Assumptions
- The canonical key remains
"Required"(PascalCase, matching MudBlazor's parameter). Changing it would
break anyone following the current README. - Target is a minor version — this adds public API. Base branch is
dev.
🛠️ Implementation plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: replace the documented magic string with a typed, discoverable builder method, and resolve the
opt-in's path asymmetry deliberately rather than by omission.
Architecture: all changes in FormCraft.ForMudBlazor; the UI-agnostic FormCraft core is untouched.
.Required(...) keeps its #190 behaviour exactly.
Tech stack: .NET 8 / 10 multi-target, Blazor, MudBlazor 9, xUnit + bUnit + Shouldly (MudBlazorTestBase).
Global constraints:
- Base branch
dev; commit asPhilippe Matray <phmatray@gmail.com>; conventional commits. TreatWarningsAsErrors=true— the build fails on any warning.dotnet test --filteris silently ignored here (MTP0001); run the whole suite.- Builders are fluent and immutable after
Build();With*configures, no side effects. - The raw-string form must keep working — this is additive, not a breaking change.
.Required(...)must NOT start emitting the HTML5 attribute again (Collection item fields emit the HTML5 Required attribute, contradicting the project's validation convention #190).
Task 1: Add the typed builder method
Files: modify FormCraft.ForMudBlazor/Extensions/FieldBuilderExtensions.cs; create FormCraft.ForMudBlazor.UnitTests/Extensions/NativeRequiredBuilderTests.cs.
Interfaces: WithNativeRequired<TModel, TValue>(this FieldBuilder<TModel, TValue> field, bool enabled = true) — declared on the general TValue overload, not string only, so numeric and date fields can use it.
- Step 1: Write failing tests: the method writes
AdditionalAttributes["Required"] == true; passingfalsewritesfalse; it returns the same builder instance for chaining. - Step 2: Run
dotnet test -c Release→ FAIL (method does not exist). - Step 3: Implement it next to
WithShrinkLabel, with XML docs stating plainly that it controls MudBlazor's native required decoration and does NOT add validation —.Required(...)does that. - Step 4: Run
dotnet test -c Release→ PASS. - Step 5: Commit:
feat(mudblazor): add a typed builder method for the native required opt-in.
Task 2: Prove it drives all three item renderers
Files: modify FormCraft.ForMudBlazor.UnitTests/Fields/CollectionRequiredTests.cs.
Interfaces: none new.
- Step 1: Write tests that a text, a numeric and a date item field configured with the typed method render
Required == trueand themud-input-requiredclass. - Step 2: Write a test that a boolean item field configured with it is inert — it renders, does not throw, and takes no notice — mirroring
CollectionAdornmentTests' adornment-inertness test. - Step 3: Run the suite → PASS. If any renderer fails, fix the renderer rather than the assertion.
- Step 4: Commit:
test(mudblazor): cover the typed required opt-in across item renderers.
Task 3: Resolve the component-path asymmetry, then document
Files: modify FormCraft.ForMudBlazor/Fields/** (if honouring it there); modify FormCraft.ForMudBlazor.UnitTests/Components/RenderPipelineParityTests.cs; modify README.md.
Interfaces: none new.
- Step 1: Decide whether the component path honours the opt-in. Default recommendation: yes — an escape hatch that works on one path only is the divergence class this library keeps re-filing.
- Step 2: Implement the decision, with a test asserting it either way. If honoured, remove the raw-
"Required"bullet from the parity test's divergence list and add it toPresentation(); if not, reword that bullet to name the typed method. - Step 3: Run the suite → PASS, with
CollectionRequiredTestsstill green (.Required(...)alone must still emit no HTML5 attribute). - Step 4: Update the README's
## 🎉 Unreleasednote to document the typed method as the supported opt-in, keeping a line that the raw string still works. - Step 5: Run
dotnet build -c Releaseanddotnet test -c Release→ both green. - Step 6: Commit:
docs(mudblazor): document the typed required opt-in.
Activity
feat(mudblazor): add a typed builder method for the native required o…
docs(mudblazor): address code-review findings on the required-field r…
fix(mudblazor): announce required fields to assistive technology (#199)…