dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.56k stars 10.05k forks source link

Blazor: Support generating ID attributes based on the binding expression similar to how Name attributes are already generated #56019

Open chrischu opened 5 months ago

chrischu commented 5 months ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

I'm currently writing my first Blazor SSR application (coming from years of experience in ASP.NET Core MVC) and there's one thing that's worse than in MVC in my opinion: in MVC it's possible to generate IDs based on the model (e.g. HtmlHelper.IdFor) while it isn't (at least to my knowledge in Blazor). Blazor has a similar mechanism with the ExpressionFormatter & HtmlFieldPrefix but I would like to be able to use the same mechanism to also generate IDs additional to names on my input fields.

Why? Because I'd like to use <label for=".." without having to always manually give out IDs. Additionally the generated IDs can then also be used in end-to-end tests to fill out forms.

Describe the solution you'd like

The "least work" (at least on your side) approach would probably be to make ExpressionFormatter & HtmlFieldPrefix public so I can build custom code to use them and generate ID attributes.

Alternatively it would be cool of course, if Blazor had some way to automate the generation of an <input> element with a corresponding <label> element, although that might be tricky, since depending on the consumer, it might be necessary to render those 2 elements in separate parts of the render tree (e.g. when using horizontal Bootstrap forms).

Additional context

No response

VahidN commented 5 months ago

This is how I do it in my custom components to access the HtmlFieldName:

public class BlazorHtmlField<T> : InputBase<T>
{
    public BlazorHtmlField(Expression<Func<T>> valueExpression)
    {
        ValueExpression = valueExpression;
        _ = base.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object?>()));
        HtmlFieldName = NameAttributeValue;
    }

    public string HtmlFieldName { get; private set; }

    protected override bool TryParseValueFromString(string? value, out T result, out string validationErrorMessage)
        => throw new NotImplementedException();
}

Usage:

<label for="@ValueField.HtmlFieldName">

@code {
   private BlazorHtmlField<string?> ValueField => new(ValueExpression);

   [Parameter] public Expression<Func<string?>> ValueExpression { get; set; } = default!;
   [Parameter] public string? Value { get; set; }
}
chrischu commented 5 months ago

@VahidN That's a pretty clever workaround, I can probably apply that in my codebase. However, it would still be cool if there was a properly supported way to do that :).

chrischu commented 5 months ago

In the end I found a pretty good workaround:

@using System.Diagnostics.CodeAnalysis

@typeparam TValue
@inherits InputBase<TValue>

@ControlTemplate(new ControlContext { Id = NameAttributeValue })

@code {

  [Parameter, EditorRequired]
  public RenderFragment<ControlContext> ControlTemplate { get; set; } = null!;

  protected override bool TryParseValueFromString (string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) {
    throw new NotImplementedException();
  }

}

It can be used like this:

<ControlContextProvider Value="Value" ValueChanged="ValueChanged" ValueExpression="ValueExpression">
  <ControlTemplate>
    @context.Id
  </ControlTemplate>
</ControlContextProvider>