Amir Imam
Amir Imam

Reputation: 1043

How to make two-way binding on Blazor component

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

Answers (4)

dani herrera
dani herrera

Reputation: 51665

Quick answer

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

  • You should to raise binding changes from children component through EventCallback<string> BindingValueChanged.
  • I chose BindingValue and BindingValueChanged as identifiers, but, you can use just Value and ValueChanged. Then will be: <MyInputComponent @bind-Value="model.Name" />

Try it at BlazorFiddle.

(Edited 2022) Full documented now at: Binding with component parameters

Edited: See Option 2 below for a clean solution:


Your control inside an EditForm

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.

Option 1: Raising from EditContext

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);

        }
    }

Option 2 (recomended): Through inheritance from InputBase

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

clamchoda
clamchoda

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

raphadesa
raphadesa

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

Major
Major

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

Related Questions