I'm developing a UI library on top of Blazor and I like to enable the bind*
syntax for my components so consumers could use it too.
Context
I've read and seen plenty of examples so on the most basic level let's say we have the following custom component named RawCustomInput.razor
:
<input type="text" value="@Value" @onchange="OnValueChanged" style="width: 5rem" />
@code {
[Parameter]
public string? Value { get; set; }
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
private async Task OnValueChanged(ChangeEventArgs args)
=> await ValueChanged.InvokeAsync(args.Value as string);
}
And another component that consumes it named InputPage.razor
like this:
@page "/input"
<PageTitle>InputPage</PageTitle>
<RawCustomInput
@bind-Value="@_name" />
@if (!string.IsNullOrEmpty(_name))
{
<p>@_name</p>
}
@code {
private string? _name = null;
}
Problem
Now, the above works but say I want to change the event from onchange
to oninput
on the consumer side? so I tried to do something like this @bind-Value:event="oninput"
but then I get the following error:
does not have a property matching the name 'oninput'
So I thought I could solve this by introducing @oninput="OnValueChanged"
to RawCustomInput.razor
which looks like this:
<input type="text" value="@Value" @onchange="OnValueChanged" @oninput="OnValueChanged" style="width: 5rem" />
@code {
[Parameter]
public string? Value { get; set; }
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
private async Task OnValueChanged(ChangeEventArgs args)
=> await ValueChanged.InvokeAsync(args.Value as string);
}
But this didn't work so I read the error again and realized that something like @bind-Value:event="ValueChanged"
might work and it worked! so what I did is add another property that looks like this:
[Parameter]
public EventCallback<string> ValueChanging { get; set; }
So now my RawCustomInput.razor
looks like this:
<input type="text" value="@Value" @onchange="OnChange" @oninput="OnInput" style="width: 5rem" />
@code {
[Parameter]
public string? Value { get; set; }
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
[Parameter]
public EventCallback<string> ValueChanging { get; set; }
private async Task OnChange(ChangeEventArgs args)
=> await ValueChanged.InvokeAsync(args.Value as string);
private async Task OnInput(ChangeEventArgs args)
=> await ValueChanging.InvokeAsync(args.Value as string);
}
And finally I could use the syntax below that worked too which is great but raises few questions.
<RawCustomInput
@bind-Value="@_name"
@bind-Value:event="ValueChanging" />
Questions
- It's possible to use the syntax
bind-{Property}="{Expression}"
for binding but is it possible to enable this syntaxbind="{Expression}"
on custom components? because when I'm trying to do something like this<RawCustomInput @bind="@_name" />
it throws with the following errordoes not have a property matching the name '@bind'
but this<input type="text" @bind="@_name" />
works, why? - It's possible to do
<input type="text" @bind="@_name" @bind:event="oninput" />
but doing<RawCustomInput @bind-Value="@_name" @bind-Value:event="oninput" />
throws with the following errordoes not have a property matching the name 'oninput'
, why? to achieve a similar thing I have to introduce additional property as I pointed above but then it's seems a bit odd that forbind:event
I need to provide the name of the event whereas forbind-{Property}:event
I need to pass a property which might makes sense but I'm not sure I understand the reason.
CodePudding user response:
First you need to understand the difference between components and elements.
<input type="text" value="@Value" ..
is an element declaration. It's html code.<RawCustomInput @bind-Value="@_name" />
is a component declaration.
Second, you are defining stuff in the Razor language, not true C#. @bind
and @bind-value
are directives to the Razor compiler, and are handled differently.
The @bind
is used to bind elements. It gets compiled by the Razor compiler into C# code like this:
__builder.AddAttribute(19, "value", BindConverter.FormatValue(this.surName));
__builder.AddAttribute(20, "onchange", EventCallback.Factory.CreateBinder(this, __value => this.surName = __value, this.surName));
Note that it uses various factory classes to build the "getter" to provide a correctly formatted string to the input value
and a "setter" to handle the generated onchange
event from the input to update the variable.
In comparison, this is the code for the component @bind-value
on a component
__builder.AddAttribute(11, "Value", RuntimeHelpers.TypeCheck<String>(this.firstName));
__builder.AddAttribute(12, "ValueChanged", RuntimeHelpers.TypeCheck<EventCallback<String>>(EventCallback.Factory.Create<String>(this, RuntimeHelpers.CreateInferredEventCallback(this, __value => this.firstName = __value, this.firstName))));
It's setting the value of the Parameter Value
and creating an anonymous function to set the value from the eventcallback ValueChanged
. There is no direct wiring into the underlying input element declared within the component. It's setting the component parameter and sinking the component event. How they are wired up is up to you the designer.
To demonstrate, here's a very different component wiring for a "checkbox":
<button @onclick=this.OnValueChanged>@this.ButtonText</button>
@code {
[Parameter] public bool Value { get; set; }
[Parameter] public EventCallback<bool> ValueChanged { get; set; }
private string ButtonCss => Value ? "btn btn-success": "btn btn-danger";
private string ButtonText => Value ? "Enabled" : "Disabled";
private async Task OnValueChanged()
=> await ValueChanged.InvokeAsync(!this.Value);
}
When you declare your components as Razor Components you are restricted by the Razor language. For 99% of components this is Ok. But, if you want to do more exotic stuff, you need to drop back to writing your component directly as a C# class.
This shows one way of handling onchange/oninput.
@if(this.ChangeOnInput)
{
<input type="text" value="@Value" @oninput="OnValueChanged" />
}
else
{
<input type="text" value="@Value" @onchange="OnValueChanged" />
}
@code {
[Parameter] public string? Value { get; set; }
[Parameter] public bool ChangeOnInput { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
private async Task OnValueChanged(ChangeEventArgs args)
=> await ValueChanged.InvokeAsync(args.Value as string);
}