|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Linq.Expressions;
using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components.Forms;
/// <summary>
/// Renders a <c><label></c> element for a specified field, reading the display name from
/// <see cref="System.ComponentModel.DataAnnotations.DisplayAttribute"/> or
/// <see cref="System.ComponentModel.DisplayNameAttribute"/> if present, or falling back to the property name.
/// </summary>
/// <remarks>
/// <para>
/// The component supports two usage patterns:
/// </para>
/// <para>
/// <strong>Nested (wrapping) pattern:</strong> When <see cref="ChildContent"/> is provided, the label wraps
/// the input component, providing implicit HTML association without requiring matching for/id attributes.
/// </para>
/// <para>
/// <strong>Non-nested pattern:</strong> When <see cref="ChildContent"/> is not provided, the label renders
/// with a <c>for</c> attribute matching the field expression. The corresponding input component must have
/// a matching <c>id</c> attribute (which is automatically generated by input components derived from
/// <see cref="InputBase{TValue}"/>).
/// </para>
/// </remarks>
/// <typeparam name="TValue">The type of the field.</typeparam>
public class Label<TValue> : IComponent
{
private RenderHandle _renderHandle;
private string? _displayName;
private string? _fieldId;
[CascadingParameter] private HtmlFieldPrefix FieldPrefix { get; set; } = default!;
/// <summary>
/// Specifies the field for which the label should be rendered.
/// </summary>
[Parameter, EditorRequired]
public Expression<Func<TValue>>? For { get; set; }
/// <summary>
/// Gets or sets the child content to be rendered inside the label element.
/// Typically this contains an input component that will be implicitly associated with the label.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
/// Gets or sets a collection of additional attributes that will be applied to the label element.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
/// <inheritdoc />
void IComponent.Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
/// <inheritdoc />
Task IComponent.SetParametersAsync(ParameterView parameters)
{
var previousFor = For;
var previousChildContent = ChildContent;
var previousAdditionalAttributes = AdditionalAttributes;
parameters.SetParameterProperties(this);
if (For is null)
{
throw new InvalidOperationException($"{GetType()} requires a value for the " +
$"{nameof(For)} parameter.");
}
// Only recalculate display name and field id if the expression changed
var displayNameChanged = false;
if (For != previousFor)
{
var newDisplayName = ExpressionMemberAccessor.GetDisplayName(For);
if (newDisplayName != _displayName)
{
_displayName = newDisplayName;
displayNameChanged = true;
}
_fieldId = FieldIdGenerator.SanitizeHtmlId(
FieldPrefix != null ? FieldPrefix.GetFieldName(For) :
ExpressionFormatter.FormatLambda(For));
}
var otherParamsChanged = ChildContent != previousChildContent || AdditionalAttributes != previousAdditionalAttributes;
if (displayNameChanged || otherParamsChanged)
{
_renderHandle.Render(BuildRenderTree);
}
return Task.CompletedTask;
}
private void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "label");
// For non-nested usage (no ChildContent), add 'for' attribute to associate with input by id
if (ChildContent is null)
{
builder.AddAttribute(1, "for", _fieldId);
}
builder.AddMultipleAttributes(2, AdditionalAttributes);
builder.AddContent(3, _displayName);
builder.AddContent(4, ChildContent);
builder.CloseElement();
}
}
|