Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to focus an element when making it visible in the same method call?

I have this (simplified) component where the user input a value and presses enter. This should cause the input to disappear until a process has completed and then the input should show again.

@if (!commandRunning)
{
    <div class="action">
        <div class="command">
            <span class="symbol">></span>
            <input @ref="input" class="in" type="text" @bind-value="@_command" @bind-value:event="oninput" @onkeypress="@(e => KeyPressed(e))">
        </div>
    </div>
}

@code {

    private string _command { get; set; }

    private bool commandRunning;

    private ElementReference input;


    private async Task KeyPressed(KeyboardEventArgs args)
    {
        if (args.Key == "Enter")
        {

            commandRunning = true;
            await Task.Delay(1000);
            commandRunning = false;

            await input.FocusAsync();
        }
    }
}

This however causes the following error:

blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Unable to focus an invalid element.
      Error: Unable to focus an invalid element.

So the input has not yet been created at the point of focusing.

Altering the code by adding StateHasChanged() before the focus call makes it work however.

commandRunning = true;
await Task.Delay(1000);
commandRunning = false;
StateHasChanged();
await input.FocusAsync();

Is this the expected behaviour? Is the changed bool not evaluated in the razor code until after the method has been executed?

like image 687
olabacker Avatar asked Sep 11 '25 07:09

olabacker


2 Answers

Is this the expected behaviour?

Yes.

There is an implied StateHasChanged() before and after an event like KeyPressed. StateHasChanged queues a render, it does not immediately execute it. The code path needs to await something for the actual rendering to happen.

So when you need multiple 'updates' in your method you do need to call it yourself.

And in your solution the Rendering is happening somewhere during the input.FocusAsync().
I would go for safe with:

commandRunning = false;
StateHasChanged();
await Task.Delay(1);   // allow the rendering to happen
await input.FocusAsync();
like image 168
Henk Holterman Avatar answered Sep 16 '25 10:09

Henk Holterman


Every event in the UI has two steps, the call to the event handler followed by a call to StateHasChanged. If the event handler returns a Task then the caller waits on it's completion before calling StateHasChanged. If it returns a void then StateHasChanged is called when the event handler yields (which may or may not be when it completes).

Let's look at what you have set up.

<input @ref="input" class="in" type="text" @bind-value="@_command" @bind-value:event="oninput" @onkeypress="@(e => KeyPressed(e))">
  1. input value is wired to _command and set to update every time the user enters a value (at every keystroke including an enter).
  2. onkeypress is wired up KeyPressed and fire on every keypress.

You have two events kicking off whenever the user enters a value.

Now to KeyPressed

    commandRunning = true;
    await Task.Delay(1000);
    commandRunning = false;

    await input.FocusAsync();

The first await yields control back to the UI thread which almost certainly updates _command and fires StateHasChanged at the completion of that event. With Commandrunning set to false the html elements are wiped out and cease to exist.

After the TaskDelay completes, commandRunning is set back to true. You then try to set the focus to input which doesn't exist (the component hasn't been re-rendered and input created). Adding the StateHasChanged before FocusAsync re-renders the component, setting up all the html elements including input. After KeyPressed completes StateHasChanged is called by the orginating event (which doesn't do anything new).

To ensure what you want to do works correctly:

    private async Task KeyPressed(KeyboardEventArgs args)
    {
        if (args.Key == "Enter")
        {
            commandRunning = true;
            // make sure the UI has re-rendered and the html is destroyed
            await InvokeAsync(StateHasChanged);
            //  Do whatever you're doing
            await Task.Delay(1000);
            commandRunning = false;
            // make sure the UI has re-rendered and the html is re-built
            await InvokeAsync(StateHasChanged);
            await input.FocusAsync();
        }
    }

[Polite] Not sure why you should want to hide and then re-display the input, but.... it's your code.

like image 25
MrC aka Shaun Curtis Avatar answered Sep 16 '25 10:09

MrC aka Shaun Curtis



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!