Reputation: 1043
I want to create custom input, so I created this component:
MyInputComponent.razor
:
<div>
<input type="text" @bind="BindingValue" />
</div>
@code {
[Parameter]
public string BindingValue { get; set; }
}
Then the usage:
<EditForm Model="model" OnValidSubmit="Submit">
<MyInputComponent BindingValue="model.Name" />
</EditForm>
@code {
User model = new User() { Name = "My Name" };
private void Submit()
{
// here I found model.Name = null;
}
}
When I debug MyInputComponent
, I found the value as I have entered. But when I submit the form, the value is null.
What is missing?
Upvotes: 74
Views: 64549
Reputation: 51665
Quoting Blazor docs:
Component parameters
Binding recognizes component parameters, where @bind-{property} can bind a property value across components.
For your page:
<EditForm Model="model" OnValidSubmit="Submit">
<MyInputComponent @bind-BindingValue="model.Name" />
</EditForm>
The child component MyInputComponent
:
<div>
<InputText type="text" @bind-Value="@BindingValue" />
</div>
@code {
private string _value;
[Parameter]
public string BindingValue
{
get => _value;
set
{
if (_value == value ) return;
_value = value;
BindingValueChanged.InvokeAsync(value);
}
}
[Parameter]
public EventCallback<string> BindingValueChanged { get; set; }
}
Notice
EventCallback<string> BindingValueChanged
.BindingValue
and BindingValueChanged
as identifiers, but, you can use just Value
and ValueChanged
. Then will be: <MyInputComponent @bind-Value="model.Name" />
(Edited 2022) Full documented now at: Binding with component parameters
Edited: See Option 2 below for a clean solution:
If you want to put your component inside an EditForm and deal with validations, or take other actions using the onchange event, you should to raise EditContext.NotifyFieldChanged
. You have 2 options to do it.
You can get EditContext
from CascadeParameter
and invoke NotifyFieldChanged
by hand:
[CascadingParameter] EditContext EditContext { get; set; } = default!;
[Parameter] public Expression<Func<string>>? ValueExpression { get; set; }
#endregion
#region bindedValue
[Parameter] public EventCallback<string> ValueChanged { get; set; }
private string _value { set; get; } = "";
[Parameter]
public string Value
{
get => _value;
set
{
if (_value == value) return;
_value = value;
ValueChanged.InvokeAsync(value);
var fieldIdentifier = FieldIdentifier.Create(ValueExpression);
EditContext.NotifyFieldChanged(fieldIdentifier);
}
}
You can inherit from InputBase<string>
and just implement TryParseValueFromString
. InputBase
will do the work for you,When you inherit from InputBase
you have Value
, ValueChanged
, EditContext
, etc.
protected override bool TryParseValueFromString(string? value, out string result, [NotNullWhen(false)] out string? validationErrorMessage)
{
result = value ?? "";
validationErrorMessage = null;
return true;
}
Upvotes: 121
Reputation: 4951
It took me a bit to figure out dani herreras recommended option so I wanted to provide some clarity for others. I wanted to change all my text inputs to Bootstrap 5.0 floating labels. Inheriting from InputBase<string>
gives us a lot to work with. @CssClass
automatically takes care of applying validation classes and @CurrentValue
gives us the @bind-Value
of the component.
InputComponent.razor
@using System.Linq.Expressions
@using Microsoft.AspNetCore.Components.Forms
@inherits InputBase<string>
<div class="form-floating mb-3">
<input class="form-control @CssClass" id="@Id" @bind="@CurrentValue">
<label for="@Id">@Label</label>
</div>
<div class="form-control-validation">
<ValidationMessage For="@ValidationFor" />
</div>
@code {
[Parameter, EditorRequired] public Expression<Func<string>> ValidationFor { get; set; } = default!;
[Parameter] public string? Id { get; set; }
[Parameter] public string? Label { get; set; }
// Note that this is only for implementing CurrentValueAsString
protected override bool TryParseValueFromString(string? value, out string result, out string validationErrorMessage)
{
result = value;
validationErrorMessage = null;
return true;
}
}
SomePage.razor
@using System.ComponentModel.DataAnnotations
<EditForm EditContext="@_editContext" OnValidSubmit=@HandleValidSubmit>
<DataAnnotationsValidator/>
<button type="submit" class="btn btn-primary">Submit</button>
<ValidationSummary />
<InputComponent @bind-Value="person.Name" ValidationFor="@(()=>person.Name)" Label="Name" ></InputComponent>
<p>Two way binded value: @person.Name</p>
</EditForm>
@code {
private class ExamplePerson
{
[Required]
public string Name { get; set; }
}
private ExamplePerson person { get; set; } = new ExamplePerson();
private EditContext _editContext;
protected override void OnInitialized()
{
_editContext = new(person);
}
private async void HandleValidSubmit()
{
}
}
Additionally, we can use the Bootstrap 5.0 class names for validation by making the following changes.
protected override void OnInitialized()
{
_editContext = new(person);
_editContext.SetFieldCssClassProvider(new BootstrapValidationClassProvider());
}
public class BootstrapValidationClassProvider : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
{
if (editContext == null)
throw new ArgumentNullException(nameof(editContext));
bool isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
if (editContext.IsModified(fieldIdentifier))
return isValid ? "is-valid" : "is-invalid";
return isValid ? string.Empty : "is-invalid";
}
}
Update: I was quite new to Blazor when I wrote this answer. Instead of inheriting InputBase<string>
we could use a typeparam
to do two-way binding on all types.
@typeparam TItem
@inherits InputBase<TItem>
.
.
.
[Parameter, EditorRequired] public Expression<Func<TItem>> ValidationFor { get; set; } = default!;
protected override bool TryParseValueFromString(string? value, out TItem result, out string validationErrorMessage)
{
result = (TItem)(object)value;
validationErrorMessage = null;
return true;
}
And then we would call the component like this
<InputComponent TItem="int" @bind-Value="person.Age" ValidationFor="@(()=>person.Age)" Label="Age" ></InputComponent>
Upvotes: 3
Reputation: 471
Using Blazor with .NET7 you can do the following:
MyCustomComponent.Razor
<input type="text" @bind:get="BindingValue" @bind:set="SetAsync">
@code {
[Parameter]
public string BindingValue { get; set; }
[Parameter]
public EventCallback<string> BindingValueChanged { get; set; }
async Task SetAsync(string value)=> await BindingValueChanged.InvokeAsync(value);
}
}
Then you can use:
<MyCustomComponent @bind-BindingValue="whateverVariable" />
Upvotes: 13
Reputation: 6658
In general the accepted answer is correct and works fine. Only thing to add is the code example uses the default name convention based Events e.g.: {PropertyName}Changed
.
[Parameter] public EventCallback<string> BindingValueChanged { get; set; }
However you can override this naming convention @bind-{Property}:event="{EventCallbackName}"
<MyInputComponent @bind-BindingValue="model.Name" @bind-BindingValue:event="OnValueChanged"/>
.....
[Parameter] public EventCallback<string> OnValueChanged { get; set; }
Upvotes: 11