简体   繁体   中英

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?

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: 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;
}

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 eg: {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; }  

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

    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";
    }
}

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" />                      

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM