Skip to main content

JavaScript Interoperability

JavaScript interoperability allows a Lagoon Blazor page or component to call JavaScript code, or to receive a callback from JavaScript.

Choosing the approach​

NeedRecommended approach
One-off call: focus, scroll, clipboard, simple JavaScript libraryIJSRuntime directly.
Persistent JavaScript object with lifecycle, DOM events to clean up, JavaScript-side stateJSInteropBase, only if this need is explicit.
Default rule

Use IJSRuntime directly. JSInteropBase is reserved for advanced components that actually need a persistent and disposable JavaScript object.

Simple approach with IJSRuntime​

JavaScript file​

Place the script in the application's web resources, then reference it like the other application scripts in the project.

wwwroot/js/sample-chart.js
window.SampleChart = window.SampleChart || {};

window.SampleChart.initialize = function (element, title) {
const paragraph = document.createElement('p');
paragraph.textContent = title;
element.appendChild(paragraph);
};

window.SampleChart.clear = function (element) {
element.replaceChildren();
};

Component markup​

The .razor file contains only the markup. C# code stays in the .razor.cs file.

SampleChart.razor
@inherits LgComponentBase

<div @ref="_container"></div>

<LgButton Text="#ClearChart" OnClick="ClearAsync" />

C# code-behind​

Inject IJSRuntime, then call JavaScript functions from the lifecycle or from UI handlers.

SampleChart.razor.cs
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;

namespace Demo.Client.Components;

public partial class SampleChart
{
private ElementReference _container;

[Inject]
private IJSRuntime JSRuntime { get; set; }

[Parameter]
public string Title { get; set; }

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Initializes the JavaScript widget when the DOM element exists.
await JSRuntime.InvokeVoidAsync("SampleChart.initialize", _container, Title);
}
}

private async Task ClearAsync(ActionEventArgs args)
{
try
{
// Delegates DOM cleanup to the JavaScript function.
await JSRuntime.InvokeVoidAsync("SampleChart.clear", _container);
}
catch (Exception ex)
{
ShowException(ex);
}
}
}

JavaScript callback to C#​

Use DotNetObjectReference and a [JSInvokable] method when JavaScript code needs to notify the Blazor component.

wwwroot/js/sample-callback.js
window.SampleCallback = window.SampleCallback || {};

window.SampleCallback.register = function (element, dotNetReference) {
element.addEventListener('click', function () {
// Notifies the Blazor component after a DOM event.
dotNetReference.invokeMethodAsync('OnJavaScriptClickAsync');
});
};
ClickablePanel.razor
@inherits LgComponentBase

<div @ref="_panel">
@ChildContent
</div>
ClickablePanel.razor.cs
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace Demo.Client.Components;

public partial class ClickablePanel : IDisposable
{
private ElementReference _panel;
private DotNetObjectReference<ClickablePanel> _dotNetReference;

[Inject]
private IJSRuntime JSRuntime { get; set; }

[Parameter]
public RenderFragment ChildContent { get; set; }

protected override void OnInitialized()
{
_dotNetReference = DotNetObjectReference.Create(this);
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Passes a .NET reference to the JavaScript script.
await JSRuntime.InvokeVoidAsync("SampleCallback.register", _panel, _dotNetReference);
}
}

[JSInvokable]
public Task OnJavaScriptClickAsync()
{
ShowInformation("#PanelClicked");
return Task.CompletedTask;
}

public void Dispose()
{
_dotNetReference?.Dispose();
}
}

Advanced approach with JSInteropBase​

JSInteropBase encapsulates the creation, loading, reloading, and disposal of a JavaScript object. The framework uses it for complex components like modals, split containers, or the HTML editor.

Usage rules​

  • Create an internal C# class that inherits from JSInteropBase.
  • Pass the JavaScript class name to the base constructor.
  • Load the object with LoadAsync() or ToggleLoadAsync(true) when the DOM element is available.
  • Release resources with Dispose() or DisposeAsync().
  • On the JavaScript side, extend Lagoon.LgControl, use addEvent() / addEvents() for events, and call super.dispose().
AdvancedWidget_JSInterop.cs
namespace Demo.Client.Components;

public partial class AdvancedWidget
{
private class JSInterop : JSInteropBase
{
public JSInterop(AdvancedWidget component, Func<JSConstructorArgs> getJSConstructorArgs)
: base(component, "AdvancedWidget", getJSConstructorArgs)
{ }

internal void Refresh(string value)
{
// Calls a method exposed by the persistent JavaScript object.
JSObject?.InvokeVoid("refresh", value);
}
}
}
wwwroot/js/advanced-widget.js
Lagoon.AdvancedWidget = class extends Lagoon.LgControl {
#element;

constructor(dotNetReference, element) {
super(dotNetReference);
this.#element = element;
this.addEvent(this.#element, 'click', this.#onClick);
}

#onClick() {
// Bubbles the event to the Blazor component.
this.invokeDotNetMethodAsync('OnWidgetClickAsync');
}

refresh(value) {
this.#element.dataset.value = value;
}

dispose() {
this.#element = null;
super.dispose();
}
}
Things to avoid

Do not use JSInteropBase for a simple JavaScript call. In that case, IJSRuntime is more readable and sufficient.