File: Forms\Label.cs
Web Access
Project: src\src\Components\Web\src\Microsoft.AspNetCore.Components.Web.csproj (Microsoft.AspNetCore.Components.Web)
// 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>&lt;label&gt;</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();
    }
}