'Blazor Textfield Oninput User Typing Delay

How can I add a delay to an event (OnInput) in Blazor ?
For example, if a user is typing in the text field and you want to wait until the user has finished typing.

Blazor.Templates::3.0.0-preview8.19405.7

Code:

@page "/"
<input type="text" @bind="Data" @oninput="OnInputHandler"/>
<p>@Data</p>

@code {    
    public string Data { get; set; }   

    public void OnInputHandler(UIChangeEventArgs e)
    {
        Data = e.Value.ToString();
    }    
}


Solution 1:[1]

Solution:

There is no single solution to your question. The following code is just one approach. Take a look and adapt it to your requirements. The code resets a timer on each keyup, only last timer raises the OnUserFinish event. Remember to dispose timer by implementing IDisposable

@using System.Timers;
@implements IDisposable;

<input type="text" @bind="Data" @bind:event="oninput" 
       @onkeyup="@ResetTimer"/>
<p >UI Data: @Data
<br>Backend Data: @DataFromBackend</p>

@code {
    public string Data { get; set; } = string.Empty;
    public string DataFromBackend { get; set; }  = string.Empty;
    private Timer aTimer = default!;
    protected override void OnInitialized()
    {
        aTimer = new Timer(1000);
        aTimer.Elapsed += OnUserFinish;
        aTimer.AutoReset = false;
    }
    void ResetTimer(KeyboardEventArgs e)
    {
        aTimer.Stop();
        aTimer.Start();        
    }    
    private async void OnUserFinish(Object? source, ElapsedEventArgs e)
    {
        // https://stackoverflow.com/a/19415703/842935
        // Call backend
        DataFromBackend = await Task.FromResult( Data + " from backend");
        await InvokeAsync( StateHasChanged );
    }
    void IDisposable.Dispose()
        =>
        aTimer?.Dispose();    
}

Use case:

One example of use case of this code is avoiding backend requests, because the request is not sent until user stops typing.

Running:

sample code running

Solution 2:[2]

This answer is the middle ground between the previous answers, i.e. between DIY and using a full-blown reactive UI framework.

It utilizes the powerful Reactive.Extensions library (a.k.a. Rx), which in my opinion is the only reasonable way to solve such problems in normal scenarios.

The solution

After installing the NuGet package System.Reactive you can import the needed namespaces in your component:

@using System.Reactive.Subjects
@using System.Reactive.Linq

Create a Subject field on your component that will act as the glue between the input event and your Observable pipeline:

@code {
    private Subject<ChangeEventArgs> searchTerm = new();
    // ...
}

Connect the Subject with your input:

<input type="text" class="form-control" @[email protected]>

Finally, define the Observable pipeline:

@code {
    // ...

    private Thing[]? things;

    protected override async Task OnInitializedAsync() {
        searchTerm
            .Throttle(TimeSpan.FromMilliseconds(200))
            .Select(e => (string?)e.Value)
            .Select(v => v?.Trim())
            .DistinctUntilChanged()
            .SelectMany(SearchThings)
            .Subscribe(ts => {
                things = ts;
                StateHasChanged();
            });
    }

    private Task<Thing[]> SearchThings(string? searchTerm = null)
        => HttpClient.GetFromJsonAsync<Thing[]>($"api/things?search={searchTerm}")
}

The example pipeline above will...

  • give the user 200 milliseconds to finish typing (a.k.a. debouncing or throttling the input),
  • select the typed value from the ChangeEventArgs,
  • trim it,
  • skip any value that is the same as the last one,
  • use all values that got this far to issue an HTTP GET request,
  • store the response data on the field things,
  • and finally tell the component that it needs to be re-rendered.

If you have something like the below in your markup, you will see it being updated when you type:

@foreach (var thing in things) {
    <ThingDisplay Item=@thing @[email protected] />
}

Additional notes

Don't forget to clean up

You should properly dispose the event subscription like so:

@implements IDisposable // top of your component

// markup

@code {
    // ...

    private IDisposable? subscription;

    public void Dispose() => subscription?.Dispose();

    protected override async Task OnInitializedAsync() {
        subscription = searchTerm
            .Throttle(TimeSpan.FromMilliseconds(200))
            // ...
            .Subscribe(/* ... */);
    }
}

Subscribe() actually returns an IDisposable that you should store and dispose along with your component. But do not use using on it, because this would destroy the subscription prematurely.

Open questions

There are some things I haven't figured out yet:

  • Is it possible to avoid calling StateHasChanged()?
  • Is it possible to avoid calling Subscribe() and bind directly to the Observable inside the markup like you would do in Angular using the async pipe?
  • Is it possible to avoid creating a Subject? Rx supports creating Observables from C# Events, but how do I get the C# object for the oninput event?

Solution 3:[3]

I have created a set of Blazor components. One of which is Debounced inputs with multiple input types and much more features. Blazor.Components.Debounce.Input is available on NuGet.

You can try it out with the demo app.

Note: currently it is in Preview. Final version is coming with .NET 5. release

Solution 4:[4]

I think this is the better solution for me, I used it for searching. Here's the code that I used.

    private DateTime timer {
      get;
      set;
    } = DateTime.MinValue;


    private async Task SearchFire(ChangeEventArgs Args) {
      if (timer == DateTime.MinValue) {
        timer = DateTime.UtcNow;
      } else {
        _ = StartSearch(Args);
        timer = DateTime.UtcNow;
      }
    }

    private async Task StartSearch(ChangeEventArgs Args) { //2000 = 2 seconeds you can change it 
      await Task.Delay(2000);
      var tot = TimeSpan.FromTicks((DateTime.UtcNow - timer).Ticks).TotalSeconds;
      if (tot > 2) {
        if (!string.IsNullOrEmpty(Args.Value.ToString())) { //Do anything after 2 seconds.

          //reset timer after finished writhing
          timer = DateTime.MinValue;
        } else {}
      } else {}
    }

Solution 5:[5]

You can avoid bind the input. Just set @oninput

<Input id="theinput" @oninput="OnTextInput" />
@code {
    public string SomeField { get; set; }
    public void OnTextInput(ChangeEventArgs e)
    {
        SomeField = e.Value.ToString();
    }
}

and set initial value in javascript (if there is).

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await JSRuntime.InvokeVoidAsync("setInitialValueById", "theinput", SomeField);
    }
}

The setInitialValueById method:

window.setInitialValueById = (elementId, value) => {
document.getElementById(elementId).value = value;}

This will resolve the known input lag issue in blazor. You can set a label value with delay if it's the case:

public async Task OnTextInput(ChangeEventArgs e)
{
    var value = e.Value.ToString();
    SomeField = value;
    await JSRuntime.InvokeVoidAsync("setLabelValue", value);
}

The setLabelValue method:

let lastInput;
window.setLabelValue = (value) => {
    lastInput = value;
    setTimeout(() => {
        let inputValue = value;
        if (inputValue === lastInput) {
            document.getElementById("theLabelId").innerHTML = inputValue;
        }
    }, 2000);
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4 Shunya
Solution 5 Ali Abdollahi