DEV Community

Cover image for JavaScript Interop with Blazor WASM
kis.stupid
kis.stupid

Posted on • Originally published at kiss-code.com on

JavaScript Interop with Blazor WASM

In last article The Role of JavaScript in Blazor I laid out how a Blazor app can leverage JavaScript without being turned into it.

Now, I'll cover Blazor WASM's JavaScript interopability feature which allows your Blazor app to execute JavaScript. This is commonly used to invoke custom JavaScript methods and browser APIs.

Did you know that it works in both directions? Your JavaScript code can invoke a C# method of your Blazor app. I'll also cover that later on, in this article or you can watch the video.

Why I use JavaScript in Blazor?

The JavaScript ecosystem has an enormous amount of already built functionality and it can do anything from dynamically updating the layout on static web pages to full-fledged, highly-interactive, single-page web apps. There is a lot of knowledge and function to be learned, borrowed, re-used, ...

That's exactly what I use it for in all of my Blazor WASM projects. I implement the JS interop feature for e.g. automatically starting carousels and other components that would be too time-consuming to rebuild. And, to access the browser APIs like navigator.share or the local storage and other useful client-side, browser functionality.

Call JavaScript from Blazor

For this demo, I scaffolded a new "Blazor Web App" with "RenderMode.Auto" interactivity set globally. The code will be available on my Patreon.

I named this project: "JavaScriptInteropWithBlazor" and it contains a server-side project with the same name and a ".Client" project.

Take the typical "scroll to top" functionality, let's add that code into the client project's wwwroot/js/custom-js.js.

1. Add the custom JavaScript code

function scrollToElement(elementId) {
    console.info("Invoked 'scrollToElement'");

    const element = document.getElementById(elementId);
    element?.scrollIntoView({
        behavior: 'smooth'
    });
}

Enter fullscreen mode Exit fullscreen mode

For demo purposes, I added a log statement simply to verify that it worked.

2. Add the script tag

Then, we'll need to make sure this custom code is included in our application. Let's add the <script> tag at the bottom of the App.razor in the server-side project.

<body id="first_element_id">
    <Routes @rendermode="@InteractiveAuto" />
    <script src="_framework/blazor.web.js"></script>

    <script src="./js/custom-js.js"></script> <!-- Here -->
</body>

Enter fullscreen mode Exit fullscreen mode

Note: We're placing this script tag at the bottom of the page so that the target elements are created before trying to manipulate them with our custom JavaScript code.

3. Create ScrollToTop component

Create a new component in the client project's Layout folder. I'll name it ScrollToTop.razor and paste in the code below.

After injecting the IJSRuntime at the top, this Blazor code can call the JavaScript method scrollToElement and pass a parameter elementId.

@inject IJSRuntime JsRuntime

<button @onclick="@(() => ScrollToElement("first_element_id"))">
    Scroll to top
</button>

@code {
    private async Task ScrollToElement(string elementId)
    {   
        await JsRuntime.InvokeVoidAsync("scrollToElement", elementId);
    }
}

Enter fullscreen mode Exit fullscreen mode

4. Add the component to a page

Don't forget to add this component on a Blazor page, I'll put it on the client's MainLayout.razor somewhere underneath the @Body.

...
    <main>
        <article class="content px-4">
            @Body
        </article>
    </main>

    <ScrollToTop /> @* Here *@
</div>

Enter fullscreen mode Exit fullscreen mode

Now, running the Blazor app, clicking the "Scroll to top" button, should log "Invoked 'scrollToElement'" in our browser's console.

Return a value from JavaScript

I can also use InvokeAsync<T> to invoke a method that returns a value. I added this code to demonstrate that.

In custom-js.js:

function scrollToElement(elementId) {
    console.info("Invoked 'scrollToElement'");

    const element = document.getElementById(elementId);
    element?.scrollIntoView({
        behavior: 'smooth'
    });

    return element == null; // <-- Here
}

Enter fullscreen mode Exit fullscreen mode

In the Blazor component's ScrollToTop method:

...
var isElementNull = await JsRuntime.InvokeAsync<bool>("scrollToElement", elementId);
Console.WriteLine("Element is null: " + isElementNull);

Enter fullscreen mode Exit fullscreen mode
Interop with Browser API

Another major reason for me to use the JavaScript interopability feature is to access the browser's APIs e.g. navigator.share.

...
await JsRuntime.InvokeVoidAsync("navigator.share", new
{
    Title = "Foo",
    Text = "Foo Bar",
    Url = "https://www.kiss-code.com/blog"
});

Enter fullscreen mode Exit fullscreen mode

After adding that to the ScrollToElement method in the Blazor app, clicking the "Scroll To Top" button should now also open the brower's share menu (if supported). This share menu will be operating system specific (Windows, Android, iOS, ...).

Find more browser APIs here.

Import JavaScript into Blazor

What I showed you above is the more common use case in which I included my custom JavaScript code by referencing it in a <script> tag at the bottom of the index page (App.razor).

A less common use case may be to encapsulate this <script> tag into a component. Unfortunately, I can't just move the script tag into the component and expect reliable behavior. A better approach is to use the JavaScript interop feature to import the script or a module.

This can be done similarly as invoking a JavaScript method, as follows:

await jsRuntime.InvokeVoidAsync("import", "./js/prism.js");

Enter fullscreen mode Exit fullscreen mode

After which you can use the methods that come with this module, for example:

await jsRuntime.InvokeVoidAsync("Prism.highlightAll");

Enter fullscreen mode Exit fullscreen mode

Call Blazor from JavaScript

To invoke a C# method in the Blazor app from JavaScript, I added the Blazor method:

[JSInvokable("DifferentNameOptional")]
public static void LogInBlazor()
{
    Console.WriteLine("Invoked: " + nameof(LogInBlazor));
}

Enter fullscreen mode Exit fullscreen mode

I added the following JavaScript snippet into the scrollToElement method in custom-js.js file:

DotNet.invokeMethod("JavaScriptInteropWithBlazor.Client", "DifferentNameOptional");

Enter fullscreen mode Exit fullscreen mode

This should work in a client-side Blazor WASM application not in a server-side one or a pre-rendered one, then it gives the following error:

There are multiple .NET runtimes present, so a default dispatcher could not be resolved. Use DotNetObject to invoke .NET instance methods.`.

Enter fullscreen mode Exit fullscreen mode

To resolve that error I had to add this object reference and pass it, the update code:

var jsObjectReference = DotNet.createJSObjectReference(window);
DotNet.invokeMethod("JavaScriptInteropWithBlazor.Client", "DifferentNameOptional", jsObjectReference);

Enter fullscreen mode Exit fullscreen mode

There is much more you can do in this direction but it can get messy, read more here: call-dotnet-from-javascript.

I first encountered this use-case in an application leveraging a strong, JavaScript-based library with a lot of custom JavaScript code built on top of it where it really proves it strength. For example, after a user interaction with the calendar, I could then pass the result to Blazor to do the necessary e.g. HTTP call afterwards. I could do this entirely in JavaScript code but I prefer to limit the amount of untyped JavaScript code in my Blazor apps.

JavaScript Interop Server-side

The JavaScript interop feature is not available for Blazor SSR (static, without interactivity) since the Blazor app needs to tap into the client-side. Don't believe ChatGPT when it suggests that, it happened to a freelance client of mine.

A Blazor app with RenderMode.InteractiveServer or a WASM hosted (pre-rendered) can use the interop but only once the interactivity kicks in after client-side render. To avoid the server-side project to throw an interop related error, we'll have to (recommended) wrap the interop calls in a conditional of the AfterRender lifecycle method. Only for interop calls that would happen OnInitialized, not the ones that happen after a button click or a not-instant user interaction. Example:

@inject IJSRuntime JsRuntime

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Task.Run(() => JsRuntime.InvokeVoidAsync("import", "./js/prism.js"));
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

I have covered the JS interop feature multiple on my YouTube channel: Keep it simple, stupid..

You can grab yourself a copy of my .NET 8 Blazor brand website which contains multiple uses of the JS interop.

My free NuGet packages contain great examples of this feature as well: Get them for FREE

Top comments (0)