Blazor scrolling to new section

Viewed 1443

I have a list component in blazor which has a collapsable create section, I want when you click to show the create section that it will display and scroll to the create section. My issue is that StateHasChanged() doesn't update the UI in time, so the element we're trying to scroll to doesn't exist yet when the javascript fires.

Obviously I can get around this with a timeout, but what is a cleaner way to do this in Blazor? Is there a way to say something like await StateUpdated()....

My code looks something like this:

@page "/createlist"

<div>
    <h3>My List</h3>
    <a @onclick="ToggleAdding">
       Create New
    </a>
</div>

<ul>
    @* list existing items... *@
</ul>

@if (IsAdding)
{
    <div id="some-create-id">
        <h3>Add Section</h3>
        @* create controls here *@
    </div>
}

@code {

    public void ToggleAdding()
    {
        IsAdding = !IsAdding;

        // Need something similar to StateHasChanged(), that is awaitable.

        JsRuntime.InvokeVoidAsync("ScrollIn", new object[] { CreateElementId });
    }
}

And the js is simply:

ScrollIn = id => {
    let element = document.getElementById(id);
    console.log(!!element ? "SCROLLIN" : "NO FOUND ELE", id);
    if (!!element) { element.scrollIntoView(); }
}
1 Answers

You need to do the JavaScript call from OnAfterRender(Async). See ASP.NET Core Razor component lifecycle for all the details.

I would actually simplify the JavaScript to be just:

ScrollElementIntoView = element => element.scrollIntoView();

And then the C# code would be something like this, without using any hardcoded ids:

@page "/scroll-test"
@inject IJSRuntime JsRuntime

<div>
    <h3>My List</h3>
    <a @onclick="ToggleAdding">
        Create New
    </a>
</div>

<ul>
    @foreach (var x in Enumerable.Range(0, 100)) {
        <li>@x</li>
    }
</ul>

@if (IsAdding)
{
    <div @ref="AddSectionRef">
        <h3>Add Section</h3>
    </div>
}

@code {
    bool IsAdding { get; set; }
    ElementReference AddSectionRef { get; set; }
    List<Func<Task>> AfterRenderAsyncJobs = new();

    private void ToggleAdding() {
        IsAdding = !IsAdding;
        AfterRenderAsyncJobs.Add(ScrollToAddSection);
        StateHasChanged();
    }

    private async Task ScrollToAddSection() {
        await JsRuntime.InvokeVoidAsync("ScrollElementIntoView", AddSectionRef);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender) {
        while (AfterRenderAsyncJobs.Any()) {
            var job = AfterRenderAsyncJobs.First();
            AfterRenderAsyncJobs.Remove(job);
            await job.Invoke();
        }
    }
}
Related