-
Notifications
You must be signed in to change notification settings - Fork 10.6k
Description
Background and Motivation
Blazor currently lacks built-in mechanisms to display property names from metadata attributes. While MVC and Razor Pages have the @Html.DisplayNameFor() helper, Blazor developers were forced to either:
- Hardcode label text (violating DRY principles and making localization difficult)
- Build custom reflection-based solutions
- Duplicate display name information across models and views
Additionally, after introducing DisplayName<TValue>, there was no built-in way to render proper HTML <label> elements that automatically associate with form inputs using either the nested pattern (wrapping) or the non-nested pattern (for/id matching).
These changes provide attribute-based solutions that follow the same familiar pattern as other Blazor form components like ValidationMessage<TValue>, enabling proper accessibility and reducing boilerplate code.
References:
- Issue DisplayNameFor support in Blazor #49147: DisplayNameFor support in Blazor
- Issue Add
Labelin addition toDisplayName#64791: Add Label in addition to DisplayName
Proposed API
namespace Microsoft.AspNetCore.Components.Forms;
+ public class DisplayName<TValue> : ComponentBase
+ {
+ [Parameter]
+ [EditorRequired]
+ public Expression<Func<TValue>>? For { get; set; }
+ }
+ public class Label<TValue> : IComponent
+ {
+ public Label();
+
+ [Parameter]
+ [EditorRequired]
+ public Expression<Func<TValue>>? For { get; set; }
+
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ [Parameter(CaptureUnmatchedValues = true)]
+ public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
+ }
public abstract class InputBase<TValue>
{
+ protected string IdAttributeValue { get; }
}Usage Examples
DisplayName Component
Basic form usage:
<EditForm Model="@product">
<div class="mb-3">
<label class="form-label">
<DisplayName For="@(() => product.Name)" />
</label>
<InputText @bind-Value="product.Name" class="form-control" />
<ValidationMessage For="@(() => product.Name)" />
</div>
</EditForm>Renders as:
<label class="form-label">Product Name</label>Table headers:
<table class="table">
<thead>
<tr>
<th><DisplayName For="@(() => product.Name)" /></th>
<th><DisplayName For="@(() => product.Price)" /></th>
<th><DisplayName For="@(() => product.ReleaseDate)" /></th>
</tr>
</thead>
</table>Label Component
Nested pattern (wrapping):
<Label For="() => model.Name">
<InputText @bind-Value="model.Name" />
</Label>Renders:
<label>
Name
<input name="model.Name" ... />
</label>Non-nested pattern (for/id):
<Label For="() => model.Name" />
<InputText @bind-Value="model.Name" />Renders:
<label for="model.Name">Name</label>
<input id="model.Name" name="model.Name" ... />Model example:
public class Product
{
[Display(Name = "Product Name")]
public string Name { get; set; }
[DisplayName("Unit Price")]
public decimal Price { get; set; }
[Display(Name = "Release Date")]
public DateTime ReleaseDate { get; set; }
}Alternative Designs
For Label component:
Two proposals were considered for the Label component:
-
Non-nested pattern only (
<label for="...">matching<input id="...">): This approach required addingidattributes to all input components and could cause breaking changes for users who manually providedidattributes that differ fromname. -
Nested pattern (preferred, implemented first): Wrapping input inside label provides implicit HTML association without requiring
for/idmatching. This approach has no breaking changes and lower implementation complexity.
The final implementation supports both patterns: nested (when ChildContent is provided) and non-nested (when no ChildContent, using for/id association).
For DisplayName component:
The component name was originally DisplayNameLabel, but was renamed to DisplayName to avoid misleading users into thinking it would render a <label> element (it only renders the plain display name text).
Risks
-
Label + Input coordination (non-nested pattern): The
LabelandInputcomponents are two separate framework components that need to coordinate. If a user overrides theidattribute on the input to a value that doesn't match whatLabelgenerates for itsforattribute, the association will break. This is documented and users who explicitly provideid/forattributes take responsibility for matching them correctly. -
Breaking changes for non-nested pattern: Users who previously manually provided
idattributes that differ from the auto-generatednamevalues may need to update their code when using the non-nestedLabelpattern. -
ID sanitization consideration: The
idattribute value should potentially be sanitized to work properly with CSS selectors (e.g.,querySelector). The implementation should match what MVC'sDefaultHtmlGeneratordoes for consistency. -
HtmlFieldPrefix handling: The
Labelcomponent needs to account forHtmlFieldPrefixsimilar to howInputBasehandlesNameAttributeValue. -
Localization: Ensured to work with the component through testing with various localization resources.
Source Justifications
| Content | Source Quote |
|---|---|
| Background - Blazor lacks DisplayNameFor | "Blazor currently lacks a built-in mechanism to display property names from metadata attributes like MVC's @Html.DisplayNameFor() helper" - PR #64636 |
| Forces developers to hardcode/build custom | "This forces developers to either: Hardcode label text (violating DRY principles and making localization difficult), Build custom reflection-based solutions, Duplicate display name information" - PR #64636 |
| DisplayName API | "public class DisplayName : ComponentBase { [Parameter] [EditorRequired] public Expression<Func>? For { get; set; } }" - PR #64636 |
| Label API | "Microsoft.AspNetCore.Components.Forms.Label, Label.For, Label.ChildContent, Label.AdditionalAttributes" - PR #64821 |
| IdAttributeValue property | "Microsoft.AspNetCore.Components.Forms.InputBase.IdAttributeValue.get -> string" - PR #64821 |
| Nested pattern usage | "Nested pattern (wrapping): <Label For="() => model.Name"><InputText @bind-Value="model.Name" /></Label>" - PR #64821 |
| Non-nested pattern usage | "Non-nested pattern (for/id): <Label For="() => model.Name" /><InputText @bind-Value="model.Name" />" - PR #64821 |
| Alternative designs - two proposals | "Proposal 1... Proposal 2 (preferred)" - Issue #64791 |
| Name change from DisplayNameLabel | "The name DisplayNameLabel might be missleading. I thought it would render a but it only renders the plain name" - @campersau in PR #64636 |
| Breaking changes risk | "The <label for="..."> only works if the input has a matching id attribute... Currently, input renders only name, not id" - Issue #64791 |
| ID sanitization risk | "We should check what DefaultHtmlGenerator does... I believe we might need/want to sanitize the id" - @javiercn review in PR #64821 |
| HtmlFieldPrefix risk | "We need to account here for the HtmlFieldPrefix I think" - @javiercn review in PR #64821 |
| Localization requirement | "We need to ensure localization works with this component" - @javiercn in PR #64636 |