Search code examples
blazorblazor-webassembly

How to make an input component validate input and call a specified callback?


I'd like to have something like:

<input id=filterString type="number" class="form-control" value="@FilterModel.FilterString" @oninput="@FilterChanged" />

but with InputNumber, to make use of validation.

So far the simplest and closest solution (maybe?) I've come across would probably be the method of creating a subclass of InputBase, passing the specified callback as a parameter, and calling it the same time InputBase's ValueChanged is called. Similar to this post.

I couldn't get that to work (I can't recall the errors specifically, but could retrieve them later if this method turns out to be recommended), and after repeated troubleshooting it started to feel clunky anyways. I've scoured the docs but still don't understand how the validation system and bindings come together. So I'm kind of lost and would appreciate some help getting this all sorted.

To sum it up:

  1. I'd like to know the simplest way to implement InputNumber (or equivalent) where I can also specify an OnInput(or equivalent) callback to do some work whenever the value is changed.
  2. It'd also be nice if someone could briefly clarify/describe the sequence of events that occur when an input is changed (i.e. what does the "pipeline" look like starting from the user interacting with an input). And in particular, when/where does InputBase do the validation? (I guess I'm just really confused how InputBase (and children) and validation work, so I'm having trouble extending them).

Thanks, all help much appreciated.


Solution

  • You can create your own component and receive a cascading parameter of type EditContext - you can then use that parameter to invoke validation, and to get any validation messages for your field.

    Now that you have full control over the input, you can hook to its @oninput method and do your work (raise other events, do more logic, invoke the .Validate() method of the edit context).

    You might need to add some more parameters to allow for value expression, maybe type param, maybe an identifier for the field name, things like that, to make it more reusable and generic, but the core is to get the edit context and control your own rendering and events.

    EDIT: added example - note that this is just one implementation and it is neither generic, nor is it perfect

    main component with the form

    @using System.ComponentModel.DataAnnotations
    
    <EditForm Model="@TheModel">
        <DataAnnotationsValidator></DataAnnotationsValidator>
        @TheModel.MyProperty
        <br />
        <MyNumberInput @bind-Value="@TheModel.MyProperty"
                       MySpecialEvent="@MySpecialEventHandler"
                       FieldId="@( new FieldIdentifier(TheModel, nameof(MyFormModel.MyProperty)) )" />
    
        <ValidationMessage For="@( () => TheModel.MyProperty )"></ValidationMessage>
    </EditForm>
    
    @code{
        MyFormModel TheModel { get; set; } = new MyFormModel();
    
        async Task MySpecialEventHandler(DateTime time)
        {
            Console.WriteLine($"special event fired at: {time.Millisecond}");
        }
    
        public class MyFormModel
        {
            [Required]
            [Range(3, 5)]
            public int? MyProperty { get; set; }
        }
    }
    

    child component - MyNumberInput

    @implements IDisposable
    
    <input type="number" @oninput="@OnInputHandler" value="@Value" />
    
    @code {
        [CascadingParameter]
        public EditContext TheEditContext { get; set; }
        [Parameter]
        public FieldIdentifier FieldId { get; set; }
        [Parameter]
        public int? Value { get; set; }
        [Parameter]
        public EventCallback<int?> ValueChanged { get; set; }
        [Parameter]
        public EventCallback<DateTime> MySpecialEvent { get; set; }
    
        async Task OnInputHandler(ChangeEventArgs e)
        {
            try
            {
                int val = int.Parse(e.Value.ToString());
                Value = val;
            }
            catch
            {
                Value = null;
            }
    
            //twoway binding
            await ValueChanged.InvokeAsync(Value);
            //update validation - AFTER the value is saved in the form
            TheEditContext.NotifyFieldChanged(FieldId);
            //your own event as needed
            await MySpecialEvent.InvokeAsync(DateTime.Now);
        }
    
        //some general update that may not be needed but might help if other fields update this one and you need to rerender
        void ValidationStateChanged(object sender, ValidationStateChangedEventArgs e)
        {
            StateHasChanged();
        }
    
        protected override void OnInitialized()
        {
            if (TheEditContext != null)
                TheEditContext.OnValidationStateChanged += ValidationStateChanged;
        }
    
        public void Dispose()
        {
            if (TheEditContext != null)
                TheEditContext.OnValidationStateChanged -= ValidationStateChanged;
        }
    }
    
    

    The sequence of events is something like

    1. user types a character
    2. the input component reacts as it is written (that can be usually the oninput or onchange DOM event - the standard components use onchange, by the way)
    3. the edit context is notified of the field change and validation is invoked - it comes down as a cascading parameter