07 Apr 2024
In last article The Role of JavaScript in Blazor I tried to lay 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.
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.
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
.
1. Add the custom JavaScript code
function scrollToElement(elementId) {
console.info("Invoked 'scrollToElement'");
const element = document.getElementById(elementId);
element?.scrollIntoView({
behavior: 'smooth'
});
}
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"></script> <!-- Here -->
</body>
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);
}
}
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>
Now, running the Blazor app, clicking the "Scroll to top" button, should log "Invoked 'scrollToElement'" in our browser's console.
I can also use InvokeAsync<T>
to invoke a method that returns a value. I added this code to demonstrate that.
In custom.js
:
function scrollToElement(elementId) {
console.info("Invoked 'scrollToElement'");
const element = document.getElementById(elementId);
element?.scrollIntoView({
behavior: 'smooth'
});
return element == null; // <-- Here
}
In the Blazor component's ScrollToTop
method:
...
var isElementNull = await JsRuntime.InvokeAsync<bool>("scrollToElement", elementId);
Console.WriteLine("Element is null: " + isElementNull);
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"
});
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.
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");
After which you can use the methods that come with this module, for example:
await jsRuntime.InvokeVoidAsync("Prism.highlightAll");
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));
}
I added the following JavaScript snippet into the scrollToElement
method in custom.js
file:
DotNet.invokeMethod("JavaScriptInteropWithBlazor.Client", "DifferentNameOptional");
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.`.
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);
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.
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"));
}
}
}
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
Thank you for taking the time & interest in my work.
Kind regards, Auguste @ kiss-code
I continuously build, learn and experiment with innovative technology. Allow me to share what I learn, with you.
Allow me to share what I learn, with you.
Unlock access to source code behind each article, here.