How to make an EditForm Input that binds using oninput rather than onchange?
Asked Answered
S

6

15

Suppose I want to use an EditForm, but I want the value binding to trigger every time the user types into the control instead of just on blur.

Suppose, for the sake of an example, that I want an InputNumber<int> that does this? I've tried using different means that are floating around such as bind-Value:event="oninput" with no success. I was finally able to get more or less what I wanted by copying the AspNetCore source code for InputNumber and overriding/rewriting a few things.

Here is my InputNumber<int> which accomplishes what I'm hoping for:

    public class MyInputNumber: InputNumber<int>
    {
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, "input");
            builder.AddAttribute(1, "step", "any");
            builder.AddMultipleAttributes(2, AdditionalAttributes);
            builder.AddAttribute(3, "type", "number");
            builder.AddAttribute(4, "class", CssClass);
            builder.AddAttribute(5, "value", FormatValue(CurrentValueAsString));
            builder.AddAttribute(6, "oninput", EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
            builder.CloseElement();
        }

        // Copied from AspNetCore - src/Components/Components/src/BindConverter.cs
        private static string FormatValue(string value, CultureInfo culture = null) => FormatStringValueCore(value, culture);
        // Copied from AspNetCore - src/Components/Components/src/BindConverter.cs
        private static string FormatStringValueCore(string value, CultureInfo culture)
        {
            return value;
        }
    }

note the "oninput" in the sequence 6 item was changed from "onchange" in the base InputNumber's BuildRenderTree method. I'd like to know how to:

  1. see the output of BuildRenderTree, so that I can know how to do this with Razor and/or
  2. just kind of know in general what sort of Razor syntax would be equivalent to doing this in the future.

I've gathered from comments in the AspNetCore code that this is definitely not the preferred way of doing this sort of thing, with Razor being the preferred approach. I've tested that this works in .NET Core 3 Preview 7 ASP.NET Core Hosted Blazor by subscribing to the EditContext's OnFieldChangedEvent, and can see that with this approach I get the different behavior that I'm looking for. Hopefully there is a better way.

Update

Including some more information about the problem

@using BlazorAugust2019.Client.Components;
@inherits BlazorFormsCode
@page "/blazorforms"

<EditForm EditContext="EditContext">
    <div class="form-group row">
        <label for="date" class="col-sm-2 col-form-label text-sm-right">Date: </label>
        <div class="col-sm-4">
            <KlaInputDate Id="date" Class="form-control" @bind-Value="Model.Date"></KlaInputDate>
        </div>
    </div>
    <div class="form-group row">
        <label for="summary" class="col-sm-2 col-form-label text-sm-right">Summary: </label>
        <div class="col-sm-4">
            <KlaInputText Id="summary" Class="form-control" @bind-Value="Model.Summary"></KlaInputText>
        </div>
    </div>
    <div class="form-group row">
        <label for="temperaturec" class="col-sm-2 col-form-label text-sm-right">Temperature &deg;C</label>
        <div class="col-sm-4">
            <KlaInputNumber Id="temperaturec" Class="form-control" @bind-Value="Model.TemperatureC"></KlaInputNumber>
        </div>
    </div>
    <div class="form-group row">
        <label for="temperaturef" class="col-sm-2 col-form-label text-sm-right">Temperature &deg;F</label>
        <div class="col-sm-4">
            <input type="text" id="temperaturef" class="form-control" value="@Model.TemperatureF" readonly />
        </div>
    </div>
</EditForm>
using BlazorAugust2019.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;

namespace BlazorAugust2019.Client.Pages.BlazorForms
{
    public class BlazorFormsCode : ComponentBase
    {
        public EditContext EditContext;
        public WeatherForecast Model;

        public BlazorFormsCode()
        {
            Model = new WeatherForecast()
            {
                Date = DateTime.Now,
                Summary = "Test",
                TemperatureC = 21
            };
            EditContext = new EditContext(Model);
            EditContext.OnFieldChanged += EditContext_OnFieldChanged;
        }

        private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e)
        {
            Console.WriteLine($"EditContext_OnFieldChanged - {e.FieldIdentifier.FieldName}");
        }
    }
}

What I'm looking for is the "EditContext_OnFieldChanged" event to fire when I type into the inputs. The current example works, just looking for a better way.

Sausa answered 27/7, 2019 at 17:20 Comment(3)
When you say "The current example works", do you mean that the "EditContext_OnFieldChanged" event is fired when you type into the inputs ? I think that the "EditContext_OnFieldChanged" event cannot be fired because there is no code that triggers the ValueChanged delegate defined in InputBase<T>Workaday
I'm not sure where or why its happening, but its happening. Yes, the Console.WriteLine is being hit and I'm reliably getting the name of the field that changed. I don't know, maybe its in the derived classes InputNumber, InputText etc. that I'm extending.Sausa
github.com/aspnet/AspNetCore/blob/… see line 70Sausa
W
30

For anyone wondering, you can subclass InputText to change how it renders. For example, to make it use the oninput event, create MyInputText.razor containing:

@inherits Microsoft.AspNetCore.Components.Forms.InputText
<input @attributes="@AdditionalAttributes" class="@CssClass" @bind="@CurrentValueAsString" @bind:event="oninput" />

Now when you use <MyInputText @bind-Value="@someval" /> it will behave just like InputText except it updates on each keystroke.

SteveSanderson

Workaday answered 2/8, 2019 at 12:16 Comment(5)
that's awesome! I had no idea! I wonder if inheriting an existing component overrides its BuildRenderTree method with the Razor markup you writeSausa
Sorry, I was not aware of this comment... Yes, "inheriting an existing component overrides its BuildRenderTree method with the Razor markup you write". Indeed, this is the recommended way: Use existing logic while providing a different view.Workaday
After doing this, I receive a System.ArgumentException: Object of type 'Microsoft.AspNetCore.Components.Web.KeyboardEventArgs' cannot be converted to type 'Microsoft.AspNetCore.Components.ChangeEventArgs'.Buntline
If my input is located together with other code, should @inherits and the input be abstracted to a separate component? Or @inherits will apply only to the next line?Agneta
Also it is not clear what is the benefit of inheriting from InputText, whereas I can use pure html input with the same effect.Agneta
R
8

Inheriting from a component and changing it so that it responds to the input event is now covered in the official documentation for .NET Core 3.1:

InputText based on the input event

Use the InputText component to create a custom component that uses the input event instead of the change event.

Create a component with the following markup, and use the component just as InputText is used:

razor:

@inherits InputText

<input
    @attributes="AdditionalAttributes" 
    class="@CssClass" 
    value="@CurrentValue" 
    @oninput="EventCallback.Factory.CreateBinder<string>(
        this, __value => CurrentValueAsString = __value, CurrentValueAsString)" />

The documentation also gives an example of how to inherit a generic component:

@using System.Globalization
@typeparam TValue
@inherits InputBase<TValue>

So if you then combine the two of those together you can create a component that changes the behaviour of the InputNumber component as follows:

@typeparam TNumber
@inherits InputNumber<TNumber>

<input 
   type="number"
   step="any"
   @attributes="AdditionalAttributes" 
   class="@CssClass" 
   value="@CurrentValue" 
   @oninput="EventCallback.Factory.CreateBinder<string>(
        this, __value => CurrentValueAsString = __value, CurrentValueAsString)" />

Including the type="number" part will give the same behaviour as the existing InputNumber (only allowing numeric character entry, using the up and down arrows to increment/decrement etc.)

If you put the code above in a file called say InputNumberUpdateOnInput.razor in the Shared folder of the Blazor project that component can then be used in the same way you'd use a normal InputNumber, for example:

<InputNumberUpdateOnInput class="form-control text-right" @bind-Value="@invoiceLine.Qty" />

If you want to control the number of decimal places the component will allow you'd need to make the step value a parameter that you pass into the component. There's more on step in this answer.

Roseannroseanna answered 2/4, 2020 at 14:53 Comment(0)
W
4

Try this:

 <input type="text" id="example1" @bind-value="@value" @bind-value:event="oninput" />

More...

Your approach is not recommended. You should subclass your components using Razor.

The following is a sample that should work. It can redirect you how to get a solution.

A solution to wrap an InputText:

NewInputText.razor

<div class="form-group">
    <label class="col-form-label">@Label</label>
    <InputText Class="form-control" Value="@Value" ValueChanged="@ValueChanged" ValueExpression="@ValueExpression"></InputText>
</div>

@functions {
    [Parameter] string Label { get; set; }
    [Parameter] string Value { get; set; }
    [Parameter] EventCallback<string> ValueChanged { get; set; }
    [Parameter] Expression<Func<string>> ValueExpression { get; set; }
}

Index.razor

<span>Name of the category: @category.Name</span>
<EditForm Model="@category">
    <NewInputText @bind-Value="@category.Name"/>
</EditForm>

You may also inherit from InputBase directly like so:

 @inherits InputBase<string>
 <input type="number" bind="@CurrentValue" id="@Id" class="@CssClass" />

Hope this helps...

Workaday answered 27/7, 2019 at 18:24 Comment(5)
I can't help but notice each of your different solutions is only half the solution. I want to, at some point, derive from InputBase as my assumption is that is required for use in the EditForm (as well as to have things like EditContext out of the box) but I also want to use OnInput. Do you have a tested solution that fires the EditContext.OnFieldChanged event when typing into the input? Also be aware that I'm using an EditContext instead of the Model parameter.Sausa
Did you try the first solution ?Workaday
I did, and it doesn't work as I specified in the OP.Sausa
unless you mean the <input> solution where I don't make a component at all. That won't work because I need an edit context for other reasons. Edit: did test it, also doesn't work as expectedSausa
Updated the OP to clarify what I'm looking for.Sausa
F
3

I found that one can do that if he/she avoids using @bind and instead, bind manually like this:

<InputText Value=@MyValue @oninput=@HandleInputChange ValueExpression=@(() => MyValue) />

@{
  // This will change for every character which is entered/removed from the input
  Public string MyValue { get; set; }

  void HandleInputChange(ChangeEventArgs args)
  {
    MyValue = args.Value.ToString();
  }

}

I still don't really understand why this works. I think it is because of the AdditionalAttributes parameter. It passes the oninput attribute to the html <input> element which updates your value, so you are not relying anymore on the ValueChanged callback to update your value.

Fairlead answered 31/1, 2021 at 19:58 Comment(0)
S
1

Alright, I've figured this out after poking around the source code looking particularly at properties CurrentValueAsString and CurrentValue. This is the solution I've come up with for making a text input that properly fires field changed events oninput:

    public class InputTextCode : InputBase<string>
    {
        protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
        {
            result = value;
            validationErrorMessage = null;
            return true;
        }
    }
@inherits InputTextCode;

<input type="text" id="@Id" class="@Class" @bind-value="CurrentValueAsString" @bind-value:event="oninput" />

it would be really nice if this could be an easily configurable option on Input components out of the box, but at least there is a way to do it that doesn't make my skin crawl.

Sausa answered 28/7, 2019 at 5:6 Comment(1)
Did you find any configurable/easier way to do it ?Tse
S
0

I usually solve it like this for InputNumber if you want to keep the default onchange event:

<p>
    <InputNumber @bind-Value="myValue" @oninput="@(e => Int32.TryParse(e.Value.ToString(), out myValue))" />
</p>
<p>
    myValue: @myValue
</p>

@code {
    private int myValue = 0;
}

Nullable int:

<p>
    <InputNumber @bind-Value="myValue" @oninput="@(e => myValue = int.TryParse(e.Value.ToString(), out int tmp) ? (int?)tmp : null)" />
</p>
<p>
    myValue: @myValue
</p>

@code {
    private int? myValue = 0;
}

If you want to keep InputText default onchange event but still react to oninput I usually do it like this: @oninput="@(e => <bind-value variable> = e.Value.ToString())"

For example:

<InputText @bind-value="@Model.Username" @oninput="@(e => Model.Username = e.Value.ToString())" />

If you wish to use the oninput event instead of the onchange event then here is a complete example from https://learn.microsoft.com/ for .NET 7:

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

public class ExampleModel
{
    [Required]
    [StringLength(10, ErrorMessage = "Name is too long.")]
    public string? Name { get; set; }
}

Shared/CustomInputText.razor:

@inherits InputText

<input @attributes="AdditionalAttributes" 
       class="@CssClass" 
       @bind="CurrentValueAsString" 
       @bind:event="oninput" />

Pages/FormExample7.razor:

@page "/form-example-7"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample7> Logger

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <CustomInputText @bind-Value="exampleModel.Name" />

    <button type="submit">Submit</button>
</EditForm>

<p>
    CurrentValue: @exampleModel.Name
</p>

@code {
    private ExampleModel exampleModel = new();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-and-input-components?view=aspnetcore-7.0#inputtext-based-on-the-input-event

Sleepless answered 15/5, 2023 at 21:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.