{"id":19099937,"url":"https://github.com/LostBeard/SpawnDev.BlazorJS","last_synced_at":"2025-04-18T17:32:20.254Z","repository":{"id":64291580,"uuid":"574774543","full_name":"LostBeard/SpawnDev.BlazorJS","owner":"LostBeard","description":"Full Blazor WebAssembly and Javascript interop. Supports all Javascript data types and web browser APIs.","archived":false,"fork":false,"pushed_at":"2025-04-08T02:00:33.000Z","size":22637,"stargazers_count":123,"open_issues_count":0,"forks_count":7,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-09T06:03:12.145Z","etag":null,"topics":["blazor","blazor-webassembly","browser-api","csharp","dom","dotnet","indexeddb","interop","javascript","webassembly","webbrowser","webgl"],"latest_commit_sha":null,"homepage":"https://blazorjs.spawndev.com","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LostBeard.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["LostBeard"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":null,"buy_me_a_coffee":null,"custom":null}},"created_at":"2022-12-06T03:25:27.000Z","updated_at":"2025-04-08T01:59:56.000Z","dependencies_parsed_at":"2024-11-08T19:22:57.912Z","dependency_job_id":"6e0ef0b3-5dc3-40fb-88f6-004b9971aa2e","html_url":"https://github.com/LostBeard/SpawnDev.BlazorJS","commit_stats":{"total_commits":102,"total_committers":1,"mean_commits":102.0,"dds":0.0,"last_synced_commit":"ee6f2573eeef396f6a1f46ff2e28d6f2d6e9fe94"},"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LostBeard%2FSpawnDev.BlazorJS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LostBeard%2FSpawnDev.BlazorJS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LostBeard%2FSpawnDev.BlazorJS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LostBeard%2FSpawnDev.BlazorJS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LostBeard","download_url":"https://codeload.github.com/LostBeard/SpawnDev.BlazorJS/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248574942,"owners_count":21127093,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blazor","blazor-webassembly","browser-api","csharp","dom","dotnet","indexeddb","interop","javascript","webassembly","webbrowser","webgl"],"created_at":"2024-11-09T03:52:23.945Z","updated_at":"2025-04-18T17:32:19.337Z","avatar_url":"https://github.com/LostBeard.png","language":"C#","funding_links":["https://github.com/sponsors/LostBeard","https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=7QTATH4UGGY9U"],"categories":["javascript","csharp"],"sub_categories":[],"readme":"# SpawnDev.BlazorJS\n[![NuGet](https://img.shields.io/nuget/dt/SpawnDev.BlazorJS.svg?label=SpawnDev.BlazorJS)](https://www.nuget.org/packages/SpawnDev.BlazorJS) \n\nFull Blazor WebAssembly and Javascript interop. Create Javascript objects, access properties, call methods, and add/remove event handlers of any Javascript objects the .Net way without writing Javascript.  \n\n**[SpawnDev.BlazorJS.WebWorkers](https://github.com/LostBeard/SpawnDev.BlazorJS.WebWorkers)** is now in a separate repo [here.](https://github.com/LostBeard/SpawnDev.BlazorJS.WebWorkers)\n\n[Live Demo](https://blazorjs.spawndev.com/)  \n\n### Supported .Net Versions\n- Blazor WebAssembly .Net 6, 7, 8, and 9\n  - Tested VS Template: Blazor WebAssembly Standalone App\n- Blazor United .Net 8 (in WebAssembly project only) \n  - Tested VS Template: Blazor Web App (Interactive WebAssembly mode without prerendering)\n\n### Features:\n- Supports all web browser [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API)\n  - If we missed anything, open an issue and it will be updated ASAP.\n- Supports all web browser Javascript data types\n  - Over 350 strongly typed [JSObject](#jsobjects) wrappers ([listed here](https://blazorjs.spawndev.com/JSObjectTypeInfo)) included in BlazorJS including DOM, Crypto, WebGL, WebRTC, Atomics, TypedArrays, and Promises allow direct interaction with Javascript\n- Use Javascript libraries in Blazor without writing any Javascript code\n- BlazorJSRuntime wraps the default JSRuntime adding additional functionality\n- Create new Javascript objects directly from Blazor\n- Get and set Javascript object properties as well as access methods\n- Easily pass .Net methods to Javascript using ActionEvent, Callback.Create or Callback.CreateOne methods\n- Easily wrap your Javascript objects for direct manipulation from Blazor (No javascript required!)\n  - Create a class that inherits from [JSObject](#jsobject-base-class) and define the methods, properties, events, and constructors.\n- Supports [Promise](#promise), [Union](#union) method parameters, and passing [undefined](#undefined) to Javascript\n- Supports Tuple, ValueTuple serialization to and from a Javascript Array\n- Supports [null-conditional member access operator ?.](#null-conditional) in JS interop\n\n# Issues and Feature requests\nI'm here to help. If you find a bug or missing properties, methods, or Javascript objects please submit an issue [here](https://github.com/LostBeard/SpawnDev.BlazorJS/issues) on GitHub. I will help as soon as possible.\n\n# BlazorJSRuntime \nGetting started. Using BlazorJS requires 2 changes to your Program.cs.\n- Add the BlazorJSRuntime service with builder.Services.AddBlazorJSRuntime()\n- Initialize BlazorJSRuntime by calling builder.Build().BlazorJSRunAsync() instead of builder.Build().RunAsync()\n- Supports [null-conditional member access operator ?.](#null-conditional) in JS interop\n\n```cs\n// ... other usings\nusing SpawnDev.BlazorJS;\n\nvar builder = WebAssemblyHostBuilder.CreateDefault(args);\nbuilder.RootComponents.Add\u003cApp\u003e(\"#app\");\nbuilder.RootComponents.Add\u003cHeadOutlet\u003e(\"head::after\");\n\n// Add SpawnDev.BlazorJS.BlazorJSRuntime\nbuilder.Services.AddBlazorJSRuntime();\n\n// build and Init using BlazorJSRunAsync (instead of RunAsync)\nawait builder.Build().BlazorJSRunAsync();\n```\n\nInject into components\n```cs\n[Inject]\nBlazorJSRuntime JS { get; set; }\n```\n\nExamples uses\n```cs\n// Get and Set\nvar innerHeight = JS.Get\u003cint\u003e(\"window.innerHeight\");\nJS.Set(\"document.title\", \"Hello World!\");\n\n// Call\nvar item = JS.Call\u003cstring?\u003e(\"localStorage.getItem\", \"itemName\");\nJS.CallVoid(\"addEventListener\", \"resize\", Callback.Create(() =\u003e Console.WriteLine(\"WindowResized\"), _callBacks));\n\n// Attach events\nusing var window = JS.Get\u003cWindow\u003e(\"window\");\nwindow.OnOffline += Window_OnOffline;\n\n// AddEventListener and RemoveEventListener are supported on all EventTarget objects\nwindow.AddEventListener(\"resize\", Window_OnResize, true);\n\nwindow.RemoveEventListener(\"resize\", Window_OnResize, true);\n```\n\n## IMPORTANT NOTE - Async vs Sync Javascript calls\nSpawnDev's BlazorJSRuntime behaves differently than Microsoft's Blazor JSRuntime. SpawnDev's BlazorJSRuntime is more of a 1 to 1 mapping to Javascript. \n\nWhen calling Javascript methods that are not asynchronous and do not return a Promise you need to use the synchronous BlazorJSRuntime methods Call, CallVoid, or Get. \nUnlike the default Blazor JSRuntime which would allow the use of InvokeAsync, you must use the synchronous BlazorJSRuntime methods.\n\nUse synchronous BlazorJSRuntime calls for synchronous Javascript methods. \nBlazorJSRuntime CallAsync would throw an error if used on the below Javascript method.\n\n```js\n// Javascript\nfunction AddNum(num1, num2){\n    return num1 + num2;\n}\n```\n\n```cs\n// C#\nvar total = JS.Call\u003cint\u003e(\"AddNum\", 20, 22);\n// total == 42 here\n```\n\nUse synchronous BlazorJSRuntime calls for asynchronous Javascript methods.\n```js\n// Javascript\nasync function AddNum(num1, num2){\n    return num1 + num2;\n}\n```\n\n```cs\n// C#\nvar total = await JS.Call\u003cTask\u003cint\u003e\u003e(\"AddNum\", 20, 22);\n// total == 42 here\n```\n\n```cs\n// C#\nvar totalPromise = JS.Call\u003cPromise\u003cint\u003e\u003e(\"AddNum\", 20, 22);\nvar total = await totalPromise.ThenAsync();\n// total == 42 here\n```\n\nUse asynchronous BlazorJSRuntime calls for asynchronous Javascript methods.\n```js\n// Javascript\nasync function AddNum(num1, num2){\n    return num1 + num2;\n}\n```\n\n```cs\n// C#\nvar total = await JS.CallAsync\u003cint\u003e(\"AddNum\", 20, 22);\n// total == 42 here\n```\n\nUse asynchronous BlazorJSRuntime calls for methods that return a Promise.\n```js\n// Javascript\nfunction AddNum(num1, num2){\n    return new Promise((resolve, reject)=\u003e{\n        resolve(num1 + num2);\n    });\n}\n```\n\n```cs\n// C#\nvar total = await JS.CallAsync\u003cint\u003e(\"AddNum\", 20, 22);\n// total == 42 here\n```\n\n## NULL Conditional\nThe BlazorJSRuntime now supports [null-conditional member access operator ?.](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-).\n\nNote: The null-conditional member access operator ?. is also known as the Elvis operator.\n\nExample  \n```js\n// Javascript\nvar fruit = { \n    name: 'apple',\n    color: 'red'\n};\n```\n\nThe below `JS.Get` would throw an error because `fruit.options` does not exist, and therefore we cannot access a property of it.\n```cs\n// C#\nvar size = JS.Get\u003cint?\u003e(\"fruit.options.size\");\n// never gets here due to error because `fruit.options` does not exist\n```\n\nUsing a null conditional (the `?` in `fruit.options?.size`) prevents the error by allowing `fruit.options` to not exist (null or undefined.)\n```cs\n// C#\nvar size = JS.Get\u003cint?\u003e(\"fruit.options?.size\");\n// size == null here (default value for int?)\n```\n\n\n# IJSInProcessObjectReference extended\n\n```cs\n// Get Set\nvar window = JS.Get\u003cIJSInProcessObjectReference\u003e(\"window\");\nwindow.Set(\"myVar\", 5);\nvar myVar = window.Get\u003cint\u003e(\"myVar\");\n\n// Call\nwindow.CallVoid(\"addEventListener\", \"resize\", Callback.Create(() =\u003e Console.WriteLine(\"WindowResized\")));\n```\n\nCreate a new Javascript object\n```cs\nIJSInProcessObjectReference worker = JS.New(\"Worker\", myWorkerScript);\n```\n\n# ActionEvent\n\nUsed throughout the JSObject collection, ActionEvent allows a clean .Net style way to add and remove .Net callbacks for Javascript events.\n\nWith ActionEvent the operands += and -= can be used to attach and detach .Net callbacks to Javascript events. All reference handling is done automatically when events are added and removed.\n\nExample taken from the Window JSObject class which inherits from EventTarget.\n```cs\n// This is how ActionEvent is implemented in the Window class\npublic ActionEvent\u003cStorageEvent\u003e OnStorage { get =\u003e new ActionEvent\u003cStorageEvent\u003e(\"storage\", AddEventListener, RemoveEventListener); set { } }\n```\n\nExample event attach detach\n```cs\nvoid AttachEventHandlersExample()\n{\n    using var window = JS.Get\u003cWindow\u003e(\"window\");\n    // If this is the first time Window_OnStorage has been attached to an event a .Net reference is automatically created and held for future use and removal\n    window.OnStorage += Window_OnStorage;\n    // the window JSObject reference can safely be disposed as the .Net reference is attached to Window_OnStorage internally\n}\nvoid DetachEventHandlersExample()\n{\n    using var window = JS.Get\u003cWindow\u003e(\"window\");\n    // If this is the last reference of Window_OnStorage being removed then the .Net reference will automatically be disposed.\n    // IMPORTANT - detaching is important for preventing resource leaks. .Net references are only released when the reference count reaches zero (same number of -= as += used)\n    window.OnStorage -= Window_OnStorage;\n}\nvoid Window_OnStorage(StorageEvent storageEvent)\n{\n    Console.WriteLine($\"StorageEvent\");\n}\n```\n\n### ActionEvent arguments are optional\nMethods attached using ActionEvents are strongly typed and, like Javascript, all arguments are optional. This can improve performance as unused variables will not be brought into Blazor during the event.\n\nExample event attach detach (from above) without using any callback arguments.\n```cs\nvoid AttachEventHandlersExample()\n{\n    using var window = JS.Get\u003cWindow\u003e(\"window\");\n    window.OnStorage += Window_OnStorage;\n}\nvoid DetachEventHandlersExample()\n{\n    using var window = JS.Get\u003cWindow\u003e(\"window\");\n    window.OnStorage -= Window_OnStorage;\n}\n// The method below is not using the optional StorageEvent argument\nvoid Window_OnStorage()\n{\n    Console.WriteLine($\"StorageEvent\");\n}\n```\n### ActionEvent.On() and ActionEvent.Off()\nActionEvent has additional methods for attaching event handlers; `On` and `Off`. These methods provide support attaching event handlers with alternative parameter types.\n\nOr use the `On` and `Off` methods:\n```cs\nvoid AttachEventHandlersExample()\n{\n    using var window = JS.Get\u003cWindow\u003e(\"window\");\n    window.OnStorage.On(Window_OnStorage);\n}\nvoid DetachEventHandlersExample()\n{\n    using var window = JS.Get\u003cWindow\u003e(\"window\");\n    window.OnStorage.Off(Window_OnStorage);\n}\n// The method below is not using the optional StorageEvent argument\nvoid Window_OnStorage()\n{\n    Console.WriteLine($\"StorageEvent\");\n}\n```\n\n### Event handler parameters\nParameters of event handlers may be omitted if not required.\n\n# FuncEvent \nFuncEvent works just like ActionEvent but, as the name indicates, works with `Func` methods instead of `Action` methods to allow returning a value.\n\n# Action and Func serialization\nBlazorJS supports serialization of both Func and Action types. Internally the BlazorJS.Callback object is used. Serialized and deserialized Action and Func objects must call their DisposeJS() extension method to dispose the auto created and associated Callback and/or Function objects.\n\nAction test from BlazorJSUnitTests.cs\n```cs\nvar tcs = new TaskCompletionSource\u003cbool\u003e();\nvar callback = () =\u003e\n{\n    tcs.TrySetResult(true);\n};\nJS.CallVoid(\"setTimeout\", callback, 100);\nawait tcs.Task;\ncallback.DisposeJS();\n```\n\nFunc\u003c,\u003e test from BlazorJSUnitTests.cs\n```cs\nint testValue = 42;\nvar origFunc = new Func\u003cint, int\u003e((val) =\u003e\n{\n    return val;\n});\n// set a global Javascript var to our Func\u003cint\u003e\n// if this is the first time this Func is passed to Javascript a Callback will be created and associated to this Func for use in future serialization\n// the auto created Callback must be disposed by calling the extension method Func.DisposeJS()\nJS.Set(\"_funcCallback\", origFunc);\n// read back in our Func as an Func \n// internally a Javascript Function reference is created and associated with this Func.\n// the auto created Function must be disposed by calling the extension method Func.DisposeJS()\nvar readFunc = JS.Get\u003cFunc\u003cint, int\u003e\u003e(\"_funcCallback\");\nvar readVal = readFunc(testValue);\nif (readVal != testValue) throw new Exception(\"Unexpected result\");\n// dispose the Function created and associated with the read Func\nreadFunc.DisposeJS();\n// dispose the Callback created and associated with the original Func\norigFunc.DisposeJS();\n```\n\n# Callback\n\nThe Callback object is used to support Action and Func serialization. It can be used for a bit more control over the lifetime of you callbacks. Pass methods to Javascript using the Callback.Create and Callback.CreateOne methods. These methods use type arguments to set the types expected for incoming arguments (if any) and the expected return type (if any.) async methods are passed as Promises.\n\nPass lambda callbacks to Javascript\n```cs\nJS.Set(\"testCallback\", Callback.Create((string strArg) =\u003e {\n    Console.WriteLine($\"Javascript sent: {strArg}\");\n    // this prints \"Hello callback!\"\n}));\n```\n```js\n// in Javascript\ntestCallback('Hello callback!');\n```\n\nPass method callbacks to Javascript\n```cs\nstring SomeNetFn(string input){\n    return $\"Recvd: {input}\";\n}\n\nJS.Set(\"someNetFn\", Callback.CreateOne(SomeNetFn));\n```\n```js\n// in Javascript\nsomeNetFn('Hello callback!');\n\n// prints\nRecvd: Hello callback!\n```\n\nPass async method callbacks to Javascript\nUnder the hood, BlazorJS is returning a Promise to Javascript when the method is called\n\n```cs\nasync Task\u003cstring\u003e SomeNetFnAsync(string input)\n{\n    await Task.Delay(1000);\n    return $\"Recvd: {input}\";\n}\n\nJS.Set(\"someNetFnAsync\", Callback.CreateOne(SomeNetFnAsync));\n```\n```js\n// in Javascript\nawait someNetFnAsync('Hello callback!');\n\n// prints\nRecvd: Hello callback!\n```\n\n# JSObjects\nOver 350 Javascript types are ready to go in **SpawnDev.BlazorJS**. The classes are designed to match the Javascript [Web API interfaces](https://developer.mozilla.org/en-US/docs/Web/API#interfaces) as closely as possible. Below are some examples.\n\n## HTMLVideoElement\nFrom [HTMLVideoElement on MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement):  \n\u003e Implemented by the \\\u003cvideo\u003e element, the HTMLVideoElement interface provides special properties and methods for manipulating video objects. It also inherits properties and methods of HTMLMediaElement and HTMLElement.\n\nExample using SpawnDev.BlazorJS.JSObjects.HTMLVideoElement  \n\nFrom HTMLVideoElementExample.cs  \n```cs\n@page \"/HTMLVideoElementExample\"\n@implements IDisposable\n\n\u003cdiv\u003e\n    \u003cvideo style=\"width: 640px; height: 480px;\" controls autoplay muted @ref=videoElRef\u003e\u003c/video\u003e\n\u003c/div\u003e\n\u003cdiv\u003e\n    Source: @videoName\n\u003c/div\u003e\n\u003cdiv\u003e\n    Duration: @duration.ToString()\n\u003c/div\u003e\n\u003cdiv\u003e\n    Metadata: @metadata\n\u003c/div\u003e\n\u003cdiv\u003e\n    @foreach (var video in videos)\n    {\n        \u003cbutton onclick=\"@(() =\u003e SetSource(video.Key, video.Value))\"\u003e@video.Key\u003c/button\u003e\n    }\n\u003c/div\u003e\n\u003cpre\u003e\n    @((MarkupString)log)\n\u003c/pre\u003e\n\n@code {\n    [Inject]\n    BlazorJSRuntime JS { get; set; }\n    ElementReference? videoElRef;\n    HTMLVideoElement? videoEl = null;\n    TimeSpan duration = TimeSpan.Zero;\n    string videoName = \"\";\n    string metadata = \"\";\n    string log = \"\";\n    Dictionary\u003cstring, string\u003e videos = new Dictionary\u003cstring, string\u003e\n    {\n        { \"Elephants Dream\", \"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4\" },\n        { \"Big Buck Bunny\", \"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4\" },\n        { \"Tears Of Steel\", \"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4\" },\n        { \"Sintel\", \"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4\" },\n        { \"None\", \"\" },\n    };\n    protected override void OnAfterRender(bool firstRender)\n    {\n        if (firstRender)\n        {\n            videoEl = (HTMLVideoElement)videoElRef!;\n            videoEl.OnLoadedMetadata += VideoEl_OnLoadedMetadata;\n            videoEl.OnAbort += VideoEl_OnAbort;\n            videoEl.OnError += VideoEl_OnError;\n        }\n    }\n    void SetSource(string name, string source)\n    {\n        if (videoEl == null) return; \n        Log($\"SetSource: {name}\");\n        videoName = name;\n        videoEl.Src = source;\n        StateHasChanged();\n    }\n    void VideoEl_OnLoadedMetadata()\n    {\n        Log(\"VideoEl_OnLoadedMetadata\");\n        metadata = $\"{videoEl!.VideoWidth}x{videoEl!.VideoHeight}\";\n        duration = TimeSpan.FromSeconds(videoEl!.Duration ?? 0);\n        StateHasChanged();\n    }\n    void VideoEl_OnError()\n    {\n        Log(\"VideoEl_OnError\");\n    }\n    void VideoEl_OnAbort()\n    {\n        Log(\"VideoEl_OnAbort\");\n        metadata = $\"{videoEl!.VideoWidth}x{videoEl!.VideoHeight}\";\n        duration = TimeSpan.FromSeconds(videoEl!.Duration ?? 0);\n        StateHasChanged();\n    }\n    public void Dispose()\n    {\n        if (videoEl != null)\n        {\n            videoEl.OnLoadedMetadata -= VideoEl_OnLoadedMetadata;\n            videoEl.OnAbort -= VideoEl_OnAbort;\n            videoEl.OnError -= VideoEl_OnError;\n            videoEl.Dispose();\n            videoEl = null;\n        }\n    }\n    void Log(string message)\n    {\n        log += $\"{message}\u003cbr/\u003e\";\n    }\n}\n```\n\n## HTMLCanvasElement\n***Example coming soon***\n\n## Storage - LocalStorage, SessionStorage\n```cs\n[Inject] \nBlazorJSRuntime JS { get; set; }\n\noverride void OnInitialized()\n{\n    using Storage localStorage = JS.Get\u003cStorage\u003e(\"localStorage\");\n    localStorage.SetItem(\"myKey\", \"myValue\");\n    var myValue = localStorage.GetItem(\"myKey\");\n    // myValue == \"myValue\"\n}\n```\n\n## IndexedDB\nFrom [IndexedDB on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API):  \n\u003e IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files/blobs.\n\nThe below code was written to test various features of the IndexedDB API and this code specifically tests using a Tuple as an ObjectStore key.\n\n```cs\n[Inject] \nBlazorJSRuntime JS { get; set; }\n\npublic class Fruit\n{\n    public (byte[], long) MyKey { get; set; }\n    public string Name { get; set; }\n    public string Color { get; set; }\n}\n\noverride void OnInitialized()\n{\n    var dbName = \"garden_tuple\";\n    var dbStoreName = \"fruit\";\n    // Get the global IDBFactory (equivalent to 'JS.Get\u003cIDBFactory\u003e(\"indexedDB\")')\n    using var idbFactory = new IDBFactory();    \n    var idb = await idbFactory.OpenAsync(dbName, 2, (evt) =\u003e\n    {\n        // upgrade needed\n        using var request = evt.Target;\n        using var db = request.Result;\n        var stores = db.ObjectStoreNames;\n        if (!stores.Contains(dbStoreName))\n        {\n            using var store = db.CreateObjectStore\u003cstring, Fruit\u003e(dbStoreName, new IDBObjectStoreCreateOptions { KeyPath = \"name\" });\n            store.CreateIndex\u003c(byte[], long)\u003e(\"tuple_index\", \"myKey\");\n        }\n    });\n\n    // transaction\n    using var tx = idb.Transaction(dbStoreName, \"readwrite\");\n    using var objectStore = tx.ObjectStore\u003cstring, Fruit\u003e(dbStoreName);\n\n    // add some data\n    await objectStore.PutAsync(new Fruit { Name = \"apple\", Color = \"red\", MyKey = (new byte[] { 1, 2, 3 }, 5) });\n    await objectStore.PutAsync(new Fruit { Name = \"orange\", Color = \"orange\", MyKey = (new byte[] { 1, 2, 5 }, 5) });\n    await objectStore.PutAsync(new Fruit { Name = \"lemon\", Color = \"yellow\", MyKey = (new byte[] { 1, 2, 5 }, 5) });\n    await objectStore.PutAsync(new Fruit { Name = \"lime\", Color = \"green\", MyKey = (new byte[] { 33, 33, 45 }, 5) });\n\n    // get an IDBIndex\n    using var myIndex = objectStore.Index\u003c(byte[], long)\u003e(\"tuple_index\");\n\n    // create a range using ValueTuple type\n    using var range = IDBKeyRange\u003c(byte[], long)\u003e.Bound((new byte[] { 0, 0, 0 }, 0), (new byte[] { 5, 5, 5 }, long.MaxValue));\n\n    var included = range.Includes((new byte[] { 1, 2, 4 }, 5));\n\n    var cmpRet0 = idbFactory.Cmp\u003c(byte[], long)\u003e((new byte[] { 1, 2, 3 }, 6), (new byte[] { 1, 2, 3 }, 5));\n    var cmpRet1 = idbFactory.Cmp\u003c(byte[], long)\u003e((new byte[] { 1, 2, 3 }, 5), (new byte[] { 1, 2, 3 }, 5));\n    var cmpRet2 = idbFactory.Cmp\u003c(byte[], long)\u003e((new byte[] { 1, 2, 2 }, 5), (new byte[] { 1, 2, 3 }, 5));\n    var cmpRet3 = idbFactory.Cmp\u003c(byte[], long)\u003e((new byte[] { 1, 2, 4 }, 5), (new byte[] { 1, 2, 3 }, 4));\n\n    // getAll on IDBIndex using the above range\n    using var getAll = await myIndex.GetAllAsync(range);\n\n    // below prints \"apple\", \"orange\", \"lemon\"\n    // the \"lime\" entry's byte[] is outside of our range and therefore not included\n    foreach (var item in getAll.ToArray())\n    {\n        JS.Log(item.Name);\n    }\n\n    // get on IDBIndex. returns null if not found.\n    var get = await myIndex.GetAsync((new byte[] { 1, 2, 5 }, 5));\n    JS.Log(\"get\", get);\n\n    // getAll on ObjectStore\n    var getAllStore = await objectStore.GetAllAsync();\n    JS.Log(\"getAllStore\", getAllStore);\n\n    // IDBCursor iteration\n    using var cursor = await myIndex.OpenCursorAsync();\n    var hasData = cursor != null;\n    while (hasData)\n    {\n        var canCont1 = await cursor.CanContinue();\n        JS.Log(\"Entry\", cursor!.Value);\n        hasData = await cursor!.ContinueAsync();\n        var canCont2 = await cursor.CanContinue();\n        var nmt = true;\n    }\n    JS.Log(\"Done\");\n}\n```\n\n## Cache\nFrom [Cache on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Cache)  \n\u003e The Cache interface provides a persistent storage mechanism for Request / Response object pairs that are cached in long lived memory.  \n\n***Example coming soon***\n\n## TypedArray\nSpawnDev.BlazorJS supports all TypedArray types.  \n***Example coming soon***\n\n## Atomics\nFrom [Atomics on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics):  \n\u003e The Atomics namespace object contains static methods for carrying out atomic operations. They are used with SharedArrayBuffer and ArrayBuffer objects.\n\n\n***Example coming soon***\n\n# JSObject Base Class\n\nJSObjects are wrappers around IJSInProcessReference objects that can be passed to and from Javascript and allow strongly typed access to the underlying object.\n\nJSObject type wrapper example\n```cs\n// create a class for your Javascript object that inherits from JSObject\npublic class Window : JSObject \n{\n    // required constructor\n    public Window(IJSInProcessObjectReference _ref) : base(_ref) { }\n    public string Name { get =\u003e JSRef.Get\u003cstring\u003e(\"name\"); set =\u003e JSRef.Set(\"name\", value); }\n    public void Alert(string msg = \"\") =\u003e JSRef.CallVoid(msg);\n    // ...\n}\n\n// use the JSObject class to interact with the Javascript object\npublic void JSObjectClassTest() {\n    var w = JS.Get\u003cWindow\u003e(\"window\");\n    var randName = Guid.NewGuid().ToString();\n    // directly set the window.name property\n    w.Name = randName;\n    // verify the read back\n    if (w.Name != randName) throw new Exception(\"Interface property set/get failed\");\n}\n```\n\nUse the extended functions of IJSInProcessObjectReference to work with Javascript objects or\nuse the growing library of over 350 of the most common Javascript objects, including ones for \nWindow, Document, Storage (localStorage and sessionStorage), WebGL, WebRTC, and more in \nSpawnDev.BlazorJS.JSObjects. JSObjects are wrappers around IJSInProcessObjectReference that \nallow strongly typed use.\n\nBelow shows a section of the SpawnDev.BlazorJS.JSObjects.Window class. Window's base type, EventTarget, inherits from JSObject.\n```cs\npublic class Window : EventTarget {\n    // all JSObject types must have this constructor\n    public Window(IJSInProcessObjectReference _ref) : base(_ref) { }\n    // here is a property with both getter and setter\n    public string? Name { get =\u003e JSRef.Get\u003cstring\u003e(\"name\"); set =\u003e JSRef.Set(\"name\", value); }\n    // here is a read only property that returns another JSObject type\n    public Storage LocalStorage =\u003e JSRef.Get\u003cStorage\u003e(\"localStorage\");\n    // here are methods\n    public long SetTimeout(Callback callback, double delay) =\u003e JSRef.Call\u003clong\u003e(\"setTimeout\", callback, delay);\n    public void ClearTimeout(long requestId) =\u003e JSRef.CallVoid(\"clearTimeout\", requestId);    \n    // ... \n}\n```\n\nBelow the JSObject derived Window class is used\n```cs\n// below the JSObject derived Window class is used\nusing var window = JS.Get\u003cWindow\u003e(\"window\");\nvar randName = Guid.NewGuid().ToString();\n// set and get properties\nwindow.Name = randName;\nvar name = window.Name;\n// call methods\nwindow.Alert(\"Hello!\");\n```\n\n## Promise\nSpawnDev.BlazorJS.JSObjects.Promise - is a JSObject wrapper for the Javascript Promise class.\nPromises can be created in .Net to wrap async methods or Tasks. They are essentially Javascript's version of Task.\n\nCreate Promise from lambda method\n```cs\nvar promise = new Promise(async () =\u003e {\n    await Task.Delay(5000);\n});\n// pass to Javascript api\n\n```\nCreate Promise from lambda method with return value\n```cs\nvar promise = new Promise\u003cstring\u003e(async () =\u003e {\n    await Task.Delay(5000);\n    return \"Hello world!\";\n});\n// pass to Javascript api\n```\nCreate Promise from Task\n```cs\nvar taskSource = new TaskCompletionSource\u003cstring\u003e();\nvar promise = new Promise\u003cstring\u003e(taskSource.Task);\n// pass to Javascript api\n\n// then later resolve\ntaskSource.TrySetResult(\"Hello world!\");\n```\n\nBelow is a an example that uses Promises to utilize the [Web Locks API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) (**Note**: The below code is designed to demonstrate the use of a Promise. This is not the recommended way of using LockManager. See [WebWorkerService.Locks](#webworkerservicelocks) for more info.)\n\n```cs\nusing var navigator = JS.Get\u003cNavigator\u003e(\"navigator\");\nusing var locks = navigator.Locks;\n\nConsole.WriteLine($\"lock: 1\");\n\nusing var waitLock = locks.Request(\"my_lock\", Callback.CreateOne((Lock lockObj) =\u003e new Promise(async () =\u003e {\n    Console.WriteLine($\"lock acquired 3\");\n    await Task.Delay(5000);\n    Console.WriteLine($\"lock released 4\");\n})));\n\nusing var waitLock2 = locks.Request(\"my_lock\", Callback.CreateOne((Lock lockObj) =\u003e new Promise(async () =\u003e {\n    Console.WriteLine($\"lock acquired 5\");\n    await Task.Delay(5000);\n    Console.WriteLine($\"lock released 6\");\n})));\n\nConsole.WriteLine($\"lock: 2\");\n```\n\n## Custom JSObjects  \nImplement your own JSObject classes for Javascript objects not already available in the BlazorJS.JSObjects library.\n\nInstead of this (simple but not as reusable)\n```cs\nvar audio = JS.New(\"Audio\", \"https://some_audio_online\");\naudio.CallVoid(\"play\");\n```\n\nYou can do this...  \nCreate a custom Audio JSObject wrapper (Example only. Already exists.)\n```cs\npublic class Audio : JSObject\n{\n    // deserialization constructor\n    public Audio(IJSInProcessObjectReference _ref) : base(_ref) { }\n    \n    // constructor that accepts a string url\n    public Audio(string url) : base(JS.New(\"Audio\", url)) { }\n    \n    // method decalaration\n    public void Play() =\u003e JSRef.CallVoid(\"play\");\n}\n```\n\nThen use the Audio JSObject\n```cs\nvar audio = new Audio(\"https://some_audio_online\");\naudio.Play();\n```\n\n# Union\n## Use the Union\\\u003cT1, T2, ...\\\u003e type with method parameters for strong typing while allowing unrelated types just like in TypeScript.\n\n```cs\nvoid UnionTypeTestMethod(string varName, Union\u003cbool?, string?\u003e? unionTypeValue)\n{\n    JS.Set(varName, unionTypeValue);\n}\n\nvar stringValue = \"Hello world!\";\nUnionTypeTestMethod(\"_stringUnionValue\", stringValue);\nif (stringValue != JS.Get\u003cstring?\u003e(\"_stringUnionValue\")) throw new Exception(\"Unexpected result\");\n\nvar boolValue = true;\nUnionTypeTestMethod(\"_boolUnionValue\", boolValue);\nif (boolValue != JS.Get\u003cbool?\u003e(\"_boolUnionValue\")) throw new Exception(\"Unexpected result\");\n```\n\n\n# Undefinable\n## Use Undefinable\\\u003cT\\\u003e type to pass undefined to Javascript\nSome Javascript API calls may have optional parameters that behave differently depending on if you pass a null versus undefined. You can now retain strong typing on JSObject method calls and support passing undefined for JSObject parameters.\n\nUndefinable\\\u003cT\\\u003e type. \n\nExample from Test app unit tests\n```cs\n// an example method with a parameter that can also be null or undefined\n// T of Undefinable\u003cT\u003e must be nullable\nvoid MethodWithUndefinableParams(string varName, Undefinable\u003cbool?\u003e? window)\n{\n    JS.Set(varName, window);\n}\n\nbool? w = false;\n// test to show the value is passed normally\nMethodWithUndefinableParams(\"_willBeDefined2\", w);\nbool? r = JS.Get\u003cbool?\u003e(\"_willBeDefined2\");\nif (r != w) throw new Exception(\"Unexpected result\");\n\nw = null;\n// null defaults to passing as undefined\nMethodWithUndefinableParams(\"_willBeUndefined2\", w);\nif (!JS.IsUndefined(\"_willBeUndefined2\")) throw new Exception(\"Unexpected result\");\n\n// if you need to pass null to an Undefinable parameter use Undefinable\u003cT?\u003e.Null\nMethodWithUndefinableParams(\"_willBeNull2\", Undefinable\u003cbool?\u003e.Null);\nif (JS.IsUndefined(\"_willBeNull2\")) throw new Exception(\"Unexpected result\");\n\n// another way to pass undefined\nMethodWithUndefinableParams(\"_willAlsoBeUndefined2\", Undefinable\u003cbool?\u003e.Undefined);\nif (!JS.IsUndefined(\"_willAlsoBeUndefined2\")) throw new Exception(\"Unexpected result\");\n```\n\nIf using JSObjects you can also use JSObject.Undefined\\\u003cT\\\u003e to create an instance that will be passed to Javascript as undefined.\n\n```cs\n// Create an instance of the Window JSObject class that is revived in Javascript as undefined\nvar undefinedWindow = JSObject.Undefined\u003cWindow\u003e();\n// undefinedWindow is an instance of Window that is revived in Javascript as undefined\nJS.Set(\"_undefinedWindow\", undefinedWindow);\nvar isUndefined = JS.IsUndefined(\"_undefinedWindow\");\n// isUndefined == true here\n```\n\n# SpawnDev.BlazorJS.WebWorkers\n- SpawnDev.BlazorJS.WebWorkers has moved to its own repo: [SpawnDev.BlazorJS.WebWorkers](https://github.com/LostBeard/SpawnDev.BlazorJS.WebWorkers) \n\n# Blazor Web App compatibility\n.Net 8 introduced a new hosting model that allows mixing [Blazor server render mode](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0#interactive-server-side-rendering-interactive-ssr) and [Blazor WebAssembly render mode](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0#client-side-rendering-csr). [Prerendering](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0#prerendering) was also added to improve initial rendering times. \"Prerendering is the process of initially rendering page content on the server without enabling event handlers for rendered controls.\" \n\nOne of the primary goals of SpawnDev.BlazorJS is to give [Web API](https://developer.mozilla.org/en-US/docs/Web/API) access to Blazor WebAssembly that mirrors Javascript's own Web API. This includes calling conventions. For example, a call that is synchronous in Javascript is synchronous in Blazor, an asynchronous call is asynchronous. To provide that, SpawnDev.BlazorJS requires access to Microsoft's IJSInProcessRuntime and IJSInProcessRuntime is only available in Blazor WebAssembly.\n\n\n## Compatible ```Blazor Web App``` options:  \nAs of version 2.5.11 the BlazorJSRuntime service can be registered on the server the same way it is on the client. Support for this was enabled to allow prerendering if needed. While it can be registered on the server, the BlazorJSRuntime is not functional unless running in WebAssembly. A component that uses `BlazorJSRuntime` can use the service property `IsBrowser` or `OperatingSystem.IsBrowser()` to determine if the code is running in a browser. To give components a functional BlazorJSRuntime, let Blazor know that those components must be rendered with WebAssembly. How this is done depends on your project settings.\n\n### ```Interactive render mode``` - ```Auto (Server and WebAssembly)``` or ```WebAssembly```  \n\n### ```Interactivity location``` - ```Per page/component```  \n\nIn the Server project ```App.razor```:  \n```html\n    \u003cRoutes /\u003e\n```\n\nIn WebAssembly pages and components that require SpawnDev.BlazorJS (prerender optional):  \n```cs\n@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))\n```\n  \n### ```Interactivity location``` - ```Global```   \nIn the Server project ```App.razor``` (prerender optional):  \n```html\n    \u003cRoutes @rendermode=\"new InteractiveWebAssemblyRenderMode(prerender: false)\"  /\u003e\n```\n\n# IDisposable \nNOTE: The above code shows quick examples. Some objects implement IDisposable, such as JSObject, Callback, and IJSInProcessObjectReference types. \n\nJSObject types will dispose of their IJSInProcessObjectReference object when their finalizer is called if not previously disposed. \n\nCallback types must be disposed unless created with the Callback.CreateOne method, in which case they will dispose themselves after the first callback. Disposing a Callback prevents it from being called.\n\nIJSInProcessObjectReference does not dispose of interop resources with a finalizer and MUST be disposed when no longer needed. Failing to dispose these will cause memory leaks.\n\nIDisposable objects returned from a WebWorker or SharedWorker service are automatically disposed after the data has been sent to the calling thread.\n\n# Support for You\nIssues can be reported [here](https://github.com/LostBeard/SpawnDev.BlazorJS/issues) on GitHub. Create a new [discussion](https://github.com/LostBeard/SpawnDev.BlazorJS/discussions) to show off your projects and post your ideas. We are always here to help.\n\n# Support for Us\nSponsor us via Github Sponsors to give us more time to work on SpawnDev.BlazorJS.WebWorkers and other open source projects. Or buy us a cup of coffee via Paypal. All support is greatly appreciated! ♥\n\n[![GitHub Sponsor](https://img.shields.io/github/sponsors/LostBeard?label=Sponsor\u0026logo=GitHub\u0026color=%23fe8e86)](https://github.com/sponsors/LostBeard)\n[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=7QTATH4UGGY9U)\n\n# Thanks\nThank you to everyone who has helped support SpawnDev.BlazorJS and related projects financially, by filing issues, and by improving the code. Every little contribution helps!\n\n# Demos\nBlazorJS and WebWorkers Demo  \nhttps://blazorjs.spawndev.com/\n\nCurrent site under development using Blazor WASM  \nhttps://www.spawndev.com/  \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FLostBeard%2FSpawnDev.BlazorJS","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FLostBeard%2FSpawnDev.BlazorJS","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FLostBeard%2FSpawnDev.BlazorJS/lists"}