Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Blazor OnAfterRenderAsync Javascript

So I've been playing around with Blazor WebAssembly and I can't figure out how to solve this problem. Basically, I have a NavMenu.razor page that will get an array of NavMenuItem asynchronously from a JSON file. That works fine. Now, what I'm trying to do is add an event listener to each of the anchors that are generated from the <NavLink> tags in the below foreach loop.

The NavLink outside of that loop (the one not populated by the async function) successfully has the addSmoothScrollingToNavLinks() function applied to it correctly. The other NavLinks do not. It seems as if they are not yet in the DOM.

I'm guessing there's some weird race condition, but I don't know how to resolve it. Any ideas on how I might fix this?

NavMenu.razor

@inject HttpClient Http
@inject IJSRuntime jsRuntime

@if (navMenuItems == null)
{
    <div></div>
}
else
{
    @foreach (var navMenuItem in navMenuItems)
    {
        <NavLink class="nav-link" href="@navMenuItem.URL" Match="NavLinkMatch.All">
            <div class="d-flex flex-column align-items-center">
                <div>
                    <i class="@navMenuItem.CssClass fa-2x"></i>
                </div>
                <div class="mt-1">
                    <span>@navMenuItem.Text</span>
                </div>
            </div>
        </NavLink>
    }
}

<NavLink class="nav-link" href="#" Match="NavLinkMatch.All">
    <div class="d-flex flex-column align-items-center">
        This works!
    </div>
</NavLink>

@code {
    private NavMenuItem[] navMenuItems;

    protected override async Task OnInitializedAsync()
    {
        navMenuItems = await Http.GetJsonAsync<NavMenuItem[]>("sample-data/navmenuitems.json");
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await jsRuntime.InvokeVoidAsync("MyFunctions.addSmoothScrollingToNavLinks");
        }
    }

    public class NavMenuItem
    {
        public string Text { get; set; }

        public string URL { get; set; }

        public string CssClass { get; set; }
    }
}

index.html (underneath the webassembly.js script tag)

 <script>
        function scrollToTop() {
            window.scroll({
                behavior: 'smooth',
                left: 0,
                top: 0
            });
        }

        window.MyFunctions = {
            addSmoothScrollingToNavLinks: function () {
                let links = document.querySelectorAll("a.nav-link");
                console.log(links);
                // links only has 1 item in it here. The one not generated from the async method
                for (const link of links) {
                    link.addEventListener('click', function (event) {
                        event.preventDefault();
                        scrollToTop();
                    })
                }
            }
        }
    </script>
like image 965
KSib Avatar asked Dec 13 '19 04:12

KSib


People also ask

Can I use JavaScript in Blazor?

A Blazor app can invoke JavaScript (JS) functions from . NET methods and . NET methods from JS functions. These scenarios are called JavaScript interoperability (JS interop).

How do I add JavaScript to Blazor app?

Integrating JavaScript and Blazor is a little awkward because you can't add JavaScript to a Blazor C# file. However, you can add JavaScript to the Index. html page under the wwwroot folder in the project. That page (or any HTML page that works with Blazor), now just needs a script tag that references _framework/blazor.

What is OnAfterRenderAsync?

OnAfterRenderAsync(bool firstRender) This is the asynchronous method which is executed when the rendering of all the references to the component are populated.


1 Answers

That's because Blazor will NOT wait for the OnInitializedAsync() to complete and will start rendering the view once the OnInitializedAsync has started. See source code on GitHub:

private async Task RunInitAndSetParametersAsync()
{
    OnInitialized();
    var task = OnInitializedAsync();       // NO await here!

    if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
    {
        // Call state has changed here so that we render after the sync part of OnInitAsync has run
        // and wait for it to finish before we continue. If no async work has been done yet, we want
        // to defer calling StateHasChanged up until the first bit of async code happens or until
        // the end. Additionally, we want to avoid calling StateHasChanged if no
        // async work is to be performed.
        StateHasChanged();                  // Notify here! (happens before await)

        try
        {
            await task;                     // await the OnInitializedAsync to complete!
        }
        ...

As you see, the StateHasChanged() might start before OnInitializedAsync() is completed. Since you send a HTTP request within the OnInitializedAsync() method, , the component renders before you get the response from the sample-data/navmenuitems.json endpoint.

How to fix

You could bind event in Blazor(instead of js), and trigger the handlers written by JavaScript. For example, if you're using ASP.NET Core 3.1.x (won't work for 3.0, see PR#14509):

@foreach (var navMenuItem in navMenuItems)
{
<a class="nav-link" href="@navMenuItem.URL" @onclick="OnClickNavLink" @onclick:preventDefault>
    <div class="d-flex flex-column align-items-center">
        <div>
            <i class="@navMenuItem.CssClass fa-2x"></i>
        </div>
        <div class="mt-1">
            <span>@navMenuItem.Text</span>
        </div>
    </div>
</a>
}

@code{
    ...


    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        //if (firstRender)
        //{
        //    await jsRuntime.InvokeVoidAsync("MyFunctions.addSmoothScrollingToNavLinks");
        //}
    }
    private async Task OnClickNavLink(MouseEventArgs e)
    {
        Console.WriteLine("click link!");
        await jsRuntime.InvokeVoidAsync("MyFunctions.smoothScrolling");
    }

}

where the MyFunctions.addSmoothScrollingToNavLinks is:

smoothScrolling:function(){
    scrollToTop();
}
like image 159
itminus Avatar answered Oct 01 '22 23:10

itminus