Skip to content

Repository files navigation

BlazorJS

BlazorJS

Better JavaScript interaction for Blazor.

Run the sample with QuickRunNuGetDownloadsTarget frameworksMIT

📖 Documentation · NuGet · Run the sample


Run the sample

The repository ships a sample app with a live page for every feature. With QuickRun installed it is one click:

QuickRun

Or the manual way:

dotnet run --project BlazorJSSample

What it does

  1. A Scripts component to load JavaScript and stylesheet files per page or component, unloaded again on dispose.
  2. IJSRuntime extensions for dynamic invocation that remove the need to write JS wrapper functions for everything.
  3. Event interop to hook any browser event, plus ResizeObserver and IntersectionObserver, onto a Blazor component.
  4. Saving files through the File System Access API, streamed, with a download fallback.
  5. Clipboard, dialog and DOM helpers with the browser quirks already handled.
  6. 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.

Installation

dotnet add package BlazorJS

Open _Imports.razor and add the usings:

@usingBlazorJS
@using BlazorJS.Attributes
@using BlazorJS.JsInterop

The browser side registers itself through a Blazor JS initializer, so there is no service registration and no script tag to add.


Scripts Component

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>

Include multiple javascript files

Multiple js files can be loaded with a comma seperator ,

<Scriptssrc="js/myjsfile.js, js/myjsfile2.js"></Scripts>

Parameters

ParameterDefaultDescription
SrcOne or more files, comma separated
UnloadOnDisposetrueRemoves the elements again when the component is disposed
SourceLoadBehaviourOnAfterRenderOnInitialized, OnInitializedAsync, OnAfterRender or OnAfterRenderAsync
SourceLoadedEventCallback<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");

Extended Dynamic JS Invocation

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.

Simple Call

awaitjsRuntime.DInvokeVoidAsync(window =>window.alert("test"));

Passing Parameters

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));

Using return values

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...

Clipboard

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();

Saving files

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.

Simple event interop helper

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.

Resize and visibility observers

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.

Dialogs and small helpers

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");

BaseComponent for Js wrapper components

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.

  1. 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()};}
  1. 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);}

Browser detect

<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.

About

BlazorJS is a small package to use a Scripts Component on every page or component to load JavaScript files when not loaded, and unload automatically.

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages