Skip to content

Add a typed builder method for the native required opt-in instead of a magic string #204

Closed

Description

@phmatray

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:

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

Non-goals

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"]
Loading

Key files

  • FormCraft.ForMudBlazor/Extensions/FieldBuilderExtensions.cs — where WithVariant (133),
    WithShrinkLabel (175) and WithAdornment (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 ## 🎉 Unreleased note 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 the mud-input-required class.
  • It is available on text, numeric and date item fields (the three renderers fed by the shared helper).
  • .Required("…") alone still renders no HTML5 required attribute — 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: WithAdornment is declared on FieldBuilder<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 as Philippe Matray <phmatray@gmail.com>; conventional commits.
  • TreatWarningsAsErrors=true — the build fails on any warning.
  • dotnet test --filter is 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; passing false writes false; 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 == true and the mud-input-required class.
  • 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 to Presentation(); if not, reword that bullet to name the typed method.
  • Step 3: Run the suite → PASS, with CollectionRequiredTests still green (.Required(...) alone must still emit no HTML5 attribute).
  • Step 4: Update the README's ## 🎉 Unreleased note 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 Release and dotnet test -c Release → both green.
  • Step 6: Commit: docs(mudblazor): document the typed required opt-in.

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:mediumFix when possiblestatus:triagedClassified and ready for analysis/worktype:featureNew feature or enhancement request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions