Better JavaScript interaction for Blazor.
📖 Documentation · NuGet · Run the sample
The repository ships a sample app with a live page for every feature. With QuickRun installed it is one click:
Or the manual way:
dotnet run --project BlazorJSSample- A Scripts component to load JavaScript and stylesheet files per page or component, unloaded again on dispose.
- IJSRuntime extensions for dynamic invocation that remove the need to write JS wrapper functions for everything.
- Event interop to hook any browser event, plus
ResizeObserverandIntersectionObserver, onto a Blazor component. - Saving files through the File System Access API, streamed, with a download fallback.
- Clipboard, dialog and DOM helpers with the browser quirks already handled.
- A base component to import a module and create a JS object reference from it.
Target frameworks: net10.0, net9.0, net8.0, net7.0, net6.0 and netstandard2.1.
dotnet add package BlazorJSOpen _Imports.razor and add the usings:
@using BlazorJS
@using BlazorJS.Attributes
@using BlazorJS.JsInteropThe browser side registers itself through a Blazor JS initializer, so there is no service registration and no script tag to add.
The scripts component allows you to include every javascript file easily to your pages or components.
For example open any page like the index.razor and add
<Scripts src="js/myjsfile.js"></Scripts>This component can also load stylesheet files
<Scripts src="js/myjsfile.js,css/mystyle.css"></Scripts>Multiple js files can be loaded with a comma seperator ,
<Scripts src="js/myjsfile.js, js/myjsfile2.js"></Scripts>| Parameter | Default | Description |
|---|---|---|
Src |
– | One or more files, comma separated |
UnloadOnDispose |
true |
Removes the elements again when the component is disposed |
SourceLoadBehaviour |
OnAfterRender |
OnInitialized, OnInitializedAsync, OnAfterRender or OnAfterRenderAsync |
SourceLoaded |
– | EventCallback<string> raised per file once it finished loading |
The same from code:
await jsRuntime.LoadFilesAsync("js/chart.js", "css/chart.css");
await jsRuntime.UnloadFilesAsync("js/chart.js");The Dynamic Invocation extension for IJSRuntime allows for dynamic invocation of JavaScript functions from C#.
This extension provides a method DInvokeVoidAsync, which takes in a function to be invoked and an array of
objects to be passed as arguments to that function.
await jsRuntime.DInvokeVoidAsync(window => window.alert("test"));Only the source text of the lambda is transferred, so variables from your component do not exist in the browser. Something like this is not enough:
// DONT COPY THIS!! SAMPLE FOR NOT WORKING
await jsRuntime.DInvokeVoidAsync(window => window.alert(currentCount));Pass the parameters instead:
await jsRuntime.DInvokeVoidAsync((window, c) => window.alert(c), currentCount);
await jsRuntime.DInvokeVoidAsync((window, c, p2, p3) => window.alert(c), currentCount, param2, param3);await jsRuntime.DInvokeVoidAsync((window, c, x) =>
{
window.alert(c);
window.console.log(x);
}, currentCount, "Flo");Or add parameters with the class JSArgument, which keeps the original variable names:
var date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var name = "John";
await jsRuntime.DInvokeVoidAsync(window => window.alert(currentCount + " - " + date + name),
JSArgument.For(currentCount).And(date).And(name));All samples above are also callable with a generic argument to use return values.
var res = await jsRuntime.DInvokeAsync<string>(window => window.prompt());
Console.WriteLine(res);Results can be reused as parameters:
var hash = await jsRuntime.DInvokeAsync<string>(window =>
{
window.alert(currentCount);
return window.location.hash + "_" + currentCount;
}, new[] { JSArgument.For(currentCount) });
await jsRuntime.DInvokeVoidAsync(document => document.location.hash = hash, new[] { JSArgument.For(hash) });
// After alerting the currentCount we update the url in browser like this #1_2_3_4...navigator.clipboard only exists in a secure context. CopyToClipboardAsync uses it when available and
falls back to the legacy execCommand path otherwise, so it also works on plain http during development.
var copied = await jsRuntime.CopyToClipboardAsync("Copied with BlazorJS");
if (!copied)
await jsRuntime.AlertAsync("The browser refused the copy.");
// reading always needs a secure context and a user permission, returns null when denied
var text = await jsRuntime.ReadClipboardAsync();Blazor can read files with <InputFile>, but writing one back out is still a pile of JavaScript.
SaveFileAsync uses the File System Access API when the browser has it, so the user gets a real save
dialog and picks the location, and falls back to a plain download otherwise.
// a string, the mime type defaults to text/plain
await jsRuntime.SaveFileAsync("notes.txt", "Written by BlazorJS");
// bytes
await jsRuntime.SaveFileAsync("report.pdf", pdfBytes, "application/pdf");
// or a stream, nothing is buffered in memory twice
await using var stream = File.OpenRead(path);
var saved = await jsRuntime.SaveFileAsync("export.csv", stream, "text/csv");
if (!saved)
Console.WriteLine("The user closed the save dialog.");The content is streamed with a DotNetStreamReference, so large files also work in Blazor Server, where a
single SignalR message is capped at 32 KB. Call it from a user interaction, browsers reject a save dialog
that no click asked for.
A simple possibility to hook events, with an easy OnBlur extension for the click-outside case.
private BlazorJSEventInterop<PointerEventArgs> _jsEvent;
_jsEvent = new BlazorJSEventInterop<PointerEventArgs>(_jsRuntime);
await _jsEvent.OnBlur(OnFocusLeft, ".element-selector");
private Task OnFocusLeft(PointerEventArgs arg)
{
return Task.CompletedTask;
}You can also use it manually with any event you want to.
private BlazorJSEventInterop<PointerEventArgs> _jsEvent;
_jsEvent = new BlazorJSEventInterop<PointerEventArgs>(_jsRuntime);
await _jsEvent.AddEventListener("NameOfEvent", async args => { await YourCallBack(); }, ".element-selector");If the selector does not exist yet, a MutationObserver waits for it and attaches the listener as soon as Blazor rendered the element.
The same interop class also exposes a ResizeObserver and an IntersectionObserver. Both are disconnected
when the interop instance is disposed.
private BlazorJSEventInterop<ElementSizeArgs> _resize;
private BlazorJSEventInterop<ElementVisibilityArgs> _visibility;
_resize = new BlazorJSEventInterop<ElementSizeArgs>(_jsRuntime);
await _resize.OnResize(OnResized, ".my-chart"); // without a selector: the whole viewport
_visibility = new BlazorJSEventInterop<ElementVisibilityArgs>(_jsRuntime);
await _visibility.OnVisibilityChanged(LoadMore, "#load-more-marker", threshold: 0.5);
private Task OnResized(ElementSizeArgs args) // Width, Height, Top, Left
=> InvokeAsync(() => RedrawChart(args.Width, args.Height));
private async Task LoadMore(ElementVisibilityArgs args) // IsVisible, Ratio
{
if (args.IsVisible)
await LoadNextPage();
}Typical use cases: redrawing a canvas or chart when its container changes, and infinite scrolling or lazy loading with a marker element at the end of a list.
await jsRuntime.AlertAsync("Saved");
var ok = await jsRuntime.ConfirmAsync("Delete this item?");
var name = await jsRuntime.PromptAsync("Your name?", "Flo");
await jsRuntime.AddCss(".demo { color: #22d3ee }", "my-styles", skipIfElementExists: true);
await jsRuntime.LoadCss("css/component.css"); // from an <EmbeddedResource>
var exists = await jsRuntime.IsElementAvailableAsync("my-element-id");
await jsRuntime.RemoveElementAsync("my-element-id");
var scripts = await jsRuntime.GetLoadedScriptsAsync();
// wait for a global that a third party script defines
var ready = await jsRuntime.WaitForNamespaceAsync("google.maps");BlazorJS provides a small base component called BlazorJsBaseComponent<T> to create a JS wrapper component.
This is a simple way to create a JS object reference from a module and use it in your blazor component.
- Create a razor component
YourComponent.razor
@inherits BlazorJs.BlazorJsBaseComponent<YourComponent>
<div @ref="ElementReference">
</div>YourComponent.razor.cs
public partial class YourComponent
{
protected override string ComponentJsFile() => "./js/PathToYourComponent.js";
protected override string ComponentJsInitializeMethodName() => "initializeMethodForYourComponent";
[Parameter]
public string SomeGeneralParam { get; set; }
[Parameter, ForJs]
public int ParamForJs { get; set; } = 100;
[Parameter, ForJs("anotherParamForJsWithDifferentNameInJs")]
public int AnotherParamForJs { get; set; } = 100;
protected override async Task OnJsOptionsChanged()
{
// This method will automatically be called when a parameter marked with [ForJs] has changed
if (JsReference != null)
await JsReference.InvokeVoidAsync("setOptions", MyJsOptions());
}
private object MyJsOptions()
{
return this.AsJsObject(new
{
configValueWirthoutParam = 123,
});
}
/// <summary>
/// Gets the JavaScript arguments to pass to the component.
/// We override here because by default only the element reference and dotnet reference is passed
/// but we want to have directly the JsOptions available.
/// </summary>
public override object[] GetJsArguments() => new[] { ElementReference, CreateDotNetObjectReference(), MyJsOptions() };
}- Create your js file thats located in the path you have defined in
ComponentJsFile()
./js/PathToYourComponent.js
class YourComponent {
elementRef;
dotnet;
constructor(elementRef, dotNet, options) {
this.elementRef = elementRef;
this.dotnet = dotNet;
this.createWhatever(options);
}
createWhatever(options) {
// Do something with the options
console.log(options.paramForJs);
console.log(options.anotherParamForJsWithDifferentNameInJs);
console.log(options.configValueWirthoutParam);
}
setOptions(options) {
// Just update the options with the new ones
}
dispose() {
// Dispose everything you created
}
}
window.YourComponent = YourComponent;
// This method will be called from the BlazorJsBaseComponent and should match the name you have defined in `ComponentJsInitializeMethodName()`
export function initializeMethodForYourComponent(elementRef, dotnet, options) {
return new YourComponent(elementRef, dotnet, options);
}<BrowserDetect @bind-browserInfo="@Info"
OSVersionUpdate="v => osVersion = v"
OSArchitectureUpdate="a => architecture = a" />
@code {
public BrowserInfo Info { get; set; }
}BrowserInfo carries browser name and version, engine, operating system, screen resolution, time zone,
user agent and the IsMobile / IsAndroid / IsIPhone / IsIPad flags.
The full documentation lives at fgilde.github.io/BlazorJS. BlazorJS is MIT licensed.
