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:
@usingBlazorJS
@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
<Scriptssrc="js/myjsfile.js"></Scripts>This component can also load stylesheet files
<Scriptssrc="js/myjsfile.js,css/mystyle.css"></Scripts>Multiple js files can be loaded with a comma seperator ,
<Scriptssrc="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:
awaitjsRuntime.LoadFilesAsync("js/chart.js","css/chart.css");awaitjsRuntime.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.
awaitjsRuntime.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 WORKINGawaitjsRuntime.DInvokeVoidAsync(window =>window.alert(currentCount));Pass the parameters instead:
awaitjsRuntime.DInvokeVoidAsync((window,c)=>window.alert(c),currentCount);awaitjsRuntime.DInvokeVoidAsync((window,c,p2,p3)=>window.alert(c),currentCount,param2,param3);awaitjsRuntime.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:
vardate=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");varname="John";awaitjsRuntime.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.
varres=awaitjsRuntime.DInvokeAsync<string>(window =>window.prompt());Console.WriteLine(res);Results can be reused as parameters:
varhash=awaitjsRuntime.DInvokeAsync<string>(window =>{window.alert(currentCount);returnwindow.location.hash+"_"+currentCount;},new[]{JSArgument.For(currentCount)});awaitjsRuntime.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.
varcopied=awaitjsRuntime.CopyToClipboardAsync("Copied with BlazorJS");if(!copied)awaitjsRuntime.AlertAsync("The browser refused the copy.");// reading always needs a secure context and a user permission, returns null when deniedvartext=awaitjsRuntime.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/plainawaitjsRuntime.SaveFileAsync("notes.txt","Written by BlazorJS");// bytesawaitjsRuntime.SaveFileAsync("report.pdf",pdfBytes,"application/pdf");// or a stream, nothing is buffered in memory twiceawaitusingvarstream=File.OpenRead(path);varsaved=awaitjsRuntime.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.
privateBlazorJSEventInterop<PointerEventArgs>_jsEvent;_jsEvent=newBlazorJSEventInterop<PointerEventArgs>(_jsRuntime);await_jsEvent.OnBlur(OnFocusLeft,".element-selector");privateTaskOnFocusLeft(PointerEventArgsarg){returnTask.CompletedTask;}You can also use it manually with any event you want to.
privateBlazorJSEventInterop<PointerEventArgs>_jsEvent;_jsEvent=newBlazorJSEventInterop<PointerEventArgs>(_jsRuntime);await_jsEvent.AddEventListener("NameOfEvent",async args =>{awaitYourCallBack();},".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.
privateBlazorJSEventInterop<ElementSizeArgs>_resize;privateBlazorJSEventInterop<ElementVisibilityArgs>_visibility;_resize=newBlazorJSEventInterop<ElementSizeArgs>(_jsRuntime);await_resize.OnResize(OnResized,".my-chart");// without a selector: the whole viewport_visibility=newBlazorJSEventInterop<ElementVisibilityArgs>(_jsRuntime);await_visibility.OnVisibilityChanged(LoadMore,"#load-more-marker",threshold:0.5);privateTaskOnResized(ElementSizeArgsargs)// Width, Height, Top, Left=>InvokeAsync(()=>RedrawChart(args.Width,args.Height));privateasyncTaskLoadMore(ElementVisibilityArgsargs)// IsVisible, Ratio{if(args.IsVisible)awaitLoadNextPage();}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.
awaitjsRuntime.AlertAsync("Saved");varok=awaitjsRuntime.ConfirmAsync("Delete this item?");varname=awaitjsRuntime.PromptAsync("Your name?","Flo");awaitjsRuntime.AddCss(".demo { color: #22d3ee }","my-styles",skipIfElementExists:true);awaitjsRuntime.LoadCss("css/component.css");// from an <EmbeddedResource>varexists=awaitjsRuntime.IsElementAvailableAsync("my-element-id");awaitjsRuntime.RemoveElementAsync("my-element-id");varscripts=awaitjsRuntime.GetLoadedScriptsAsync();// wait for a global that a third party script definesvarready=awaitjsRuntime.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
publicpartialclassYourComponent{protectedoverridestringComponentJsFile()=>"./js/PathToYourComponent.js";protectedoverridestringComponentJsInitializeMethodName()=>"initializeMethodForYourComponent";[Parameter]publicstringSomeGeneralParam{get;set;}[Parameter,ForJs]publicintParamForJs{get;set;}=100;[Parameter,ForJs("anotherParamForJsWithDifferentNameInJs")]publicintAnotherParamForJs{get;set;}=100;protectedoverrideasyncTaskOnJsOptionsChanged(){// This method will automatically be called when a parameter marked with [ForJs] has changedif(JsReference!=null)awaitJsReference.InvokeVoidAsync("setOptions",MyJsOptions());}privateobjectMyJsOptions(){returnthis.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>publicoverrideobject[]GetJsArguments()=>new[]{ElementReference,CreateDotNetObjectReference(),MyJsOptions()};}- Create your js file thats located in the path you have defined in
ComponentJsFile()
./js/PathToYourComponent.js
classYourComponent{elementRef;dotnet;constructor(elementRef,dotNet,options){this.elementRef=elementRef;this.dotnet=dotNet;this.createWhatever(options);}createWhatever(options){// Do something with the optionsconsole.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()`exportfunctioninitializeMethodForYourComponent(elementRef,dotnet,options){returnnewYourComponent(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.
