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
| Need | Recommended approach |
|---|---|
| One-off call: focus, scroll, clipboard, simple JavaScript library | IJSRuntime directly. |
| Persistent JavaScript object with lifecycle, DOM events to clean up, JavaScript-side state | JSInteropBase, only if this need is explicit. |
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.
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.
@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.
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.
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');
});
};
@inherits LgComponentBase
<div @ref="_panel">
@ChildContent
</div>
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()orToggleLoadAsync(true)when the DOM element is available. - Release resources with
Dispose()orDisposeAsync(). - On the JavaScript side, extend
Lagoon.LgControl, useaddEvent()/addEvents()for events, and callsuper.dispose().
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);
}
}
}
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();
}
}
Do not use JSInteropBase for a simple JavaScript call. In that case, IJSRuntime is more readable and sufficient.