Search code examples
blazor.net-6.0

Two-way binding not working for custom input element


I am trying to create a re-usable input element called InputBox and essentially by default the component will use two-way binding for the value.

InputBox.razor

<div class="InputBox">
    <input title="@PlaceHolder" placeholder="@PlaceHolder" type="text" @bind=Value/>
</div>

InputBox.razor.cs

public partial class InputBox
{
    [Parameter]
    public string Value { get; set; }
    
    [Parameter]
    public string PlaceHolder { get; set; } = string.Empty;
}

The issue is that whenever I use this component and try to get the current value when a user clicks save, the value the user types is not updating up to the parent.

An example would be:

<InputBox Value=@username />

<button onclick="@SaveUserName"></button>

@code {
    public string username { get; set; } = "test user";

    public void SaveUserName() 
    {
        log(username); //username outputs 'test user' instead of whatever the user has typed.
    }
}

Solution

  • You are currently only setting the value with a one-way binding.

    <InputBox Value=@username />
    

    In this snippet here, you are passing the value of username into the parameter Value, but you don't have any way of updating the username property as part of a two-way binding.

    For two-way binding you need to set up an event callback.

    InputBox.razor.cs

    public partial class InputBox
    {
        [Parameter]
        public string Value { get; set; }
        
        [Parameter]
        public string PlaceHolder { get; set; } = string.Empty;
    
        // Add this event callback as a public parameter
        // When manually invoking it the name is arbitrary
        // You can set up the property to work with @bind by 
        // using a naming convention, but I can leave that for 
        // you to look into
        [Parameter]
        public EventCallback<string> OnValueChange { get; set; }
    }
    

    You can then invoke it after your raw input value has changed. This will let any subscribers know that the value has been updated.

    Note You have tagged this question with .NET 6. .NET 7 and 8 have newer ways of managing the binding pipeline, so I will show you a method that is compatible with .NET 6.

    InputBox.razor

    <div class="InputBox">
        <input 
            title="@PlaceHolder" 
            placeholder="@PlaceHolder" 
            type="text" 
            value="@Value"
            @onchange="e => OnValueChange.InvokeAsync(e.Value.ToString())"
         />
    </div>
    

    Note that I have replaced your @bind with value=... and @onchange=....

    The value=... is a one-way binding to set the value of the input from your property. The @onchange binds to the change event and creates a two-way binding. In this case the event handler is invoking your event callback, which ultimately passes the updated value back to the parent.

    The e parameter in the event handler is of type ChangeEventArgs. You can use this exact function with @oninput too for instant notifications.

    Edit

    .NET 7+

    In .NET 7 and later you have newer syntax to choose from.

    From the docs:

    In ASP.NET Core 7.0 or later, @bind:get/@bind:set modifier syntax is used to implement two-way data binding, as the next example demonstrates.

    This example comes straight from that page:

    <p>
        <input @bind:event="oninput" @bind:get="inputValue" @bind:set="OnInput" />
    </p>
    
    <p>
        <code>inputValue</code>: @inputValue
    </p>
    
    @code {
        private string? inputValue;
    
        private void OnInput(string value)
        {
            var newValue = value ?? string.Empty;
    
            inputValue = newValue.Length > 4 ? "Long!" : newValue;
        }
    }
    

    Note here how you can split @bind= into a getter (for setting the value, confusingly - think of it from the point of view of the <input />), and a setter (to trigger after effects).

    You can also choose which event you want to hook into by using @bind:event.

    .NET 8

    From .NET 8 you can use @bind:after=... along with @bind=... so that you can trigger an async callback while still using simple two-way binding syntax.

    This example also comes straight from the page linked above.

    <ChildBind @bind-Year="year" @bind-Year:after="YearUpdated" />
    
    @code {
        ...
    
        private async Task YearUpdated()
        {
            ... = await ...;
        }
    }
    

    So you can continue to use @bind=... to perform two-way binding, and also use @bind:after=... to trigger an event callback that bubbles the change up the call stack.