Using JS Object References in Blazor WASM to wrap JS libraries
With .NET 5 Blazor WebAssembly also got a lot of great improvements. Among these are IJSObjectReference
s (JavaScript Object References). These often get referenced in the changelogs and articles as JavaScript Isolation. The name indicates that this can only be used to isolate JavaScript sources similar to CSS Isolation. But IJSObjectReference
can do so much more than just isolation. In this article, we will show what IJSObjectReference
is and how it can be used. This article is part of our Blazor JS Wrapping series.
Recap
In the previous articles, we have made a wrapper in Blazor WASM for the JS library Popper. The other articles in this series are: Wrapping JavaScript libraries in Blazor WebAssembly/WASM and Call anonymous C# functions from JS in Blazor WASM.
The Popper library can place DOM elements in relation to each other while being smart about it. It is used in big frameworks like Bootstrap. In the previous articles, we got the constructor for the library done. In this constructor, we could parse an Options
argument which included an Enum
. We have made a custom serializer and deserializer for Enums with DiscriptionsAttribute
s. In the end, we also added the capability to call C# delegates from Popper. This resulted in the following:
IJSObjectReference
IJSObjectReference
was added in .NET 5 and enables the JSInterop
to reference JavaScript objects. This can both be done by invoking the import
function with a reference to a JS file that exports some functions. But it also enables you to reference any JavaScript objects that you might return from your JavaScript. We will use this to access the complex objects that the Popper library returns and of course use it to reference our JavaScript wrapper file.
Switching to use .NET 5
We first need to move to .NET 5 in order to use IJSObjectReference
s. First, we need to download the .NET 5.0 SDK and then update the project. We do this in the project file by updating it from the following:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.0" />
</ItemGroup>
</Project>
to
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.0" PrivateAssets="all" />
<PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
</ItemGroup>
</Project>
Migrating the existing solution to using IJSObjectReference
The next step is to move what we already have to use IJSObjectReference
s.
First, we take a look at the JS file that is the first level of wrapping for the Popper library. This is what we have:
window.PopperWrapper = {
createPopper: function (reference, popper, options, objRef) {
options.onFirstUpdate = (state) => {
const stateCopy = {
placement: state.placement
}
objRef.invokeMethodAsync('CallOnFirstUpdate', stateCopy)
};
if (options != null) {
Popper.createPopper(reference, popper, options);
} else {
Popper.createPopper(reference, popper);
}
}
}
We update this to the following:
export function createPopper(reference, popper, options, objRef)
{
options.onFirstUpdate = (state) => {
const stateCopy = {
placement: state.placement
}
objRef.invokeMethodAsync('CallOnFirstUpdate', stateCopy)
};
Popper.createPopper(reference, popper, options);
}
We have simply changed it so that the createPopper
function is exported instead of being added as a property to window
. We also removed the null check for the options argument since this would complicate further development and actually could cause an error in our C# code if we parsed null.
Now we just need to update the function that calls createPopper so that it uses the export
.
public async Task CreatePopperAsync(ElementReference reference, ElementReference popper, Options options)
{
objRef = DotNetObjectReference.Create(options);
var popperWrapper = await jSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/PopperWrapper.js");
await popperWrapper.InvokeAsync<IJSObjectReference>("createPopper", reference, popper, options, objRef);
}
What we have changed is that we first call "import"
with the path to our PopperWrapper.js
file. This works just like the import
function in JavaScript (psst... it is just that, the new thing is that the respons from this has a type in C#). The return type IJSObjectReference
is an object that can call all the functions that was exported from the imported file. We use this in the next line where we invoke "createPopper"
as we have done previously. Now that we import the JS file from within code we don't need to reference it in our index.html
file anymore so we can remove the following line:
<script src="js/PopperWrapper.js"></script>
Now we have moved the existing solution to using IJSObjectReference
. What we have done is already a great improvement. First advantage is that the functions we declare are only available when we import it and doesn't have the problem that someone might use the same name for their property in window
. Another benefit is that we only load the file when we need it similar to lazy loading of DLLs.
Returning a JS object
A great advantage of IJSObjectReference
is that we can also reference instances of JS objects. The createPopper
JS function actually returns an instance that reference that popper. With this we can access the state of the instance and update it using some functions. It is defined as so:
type Instance = {
state: State,
destroy: () => void,
forceUpdate: () => void,
update: () => Promise<$Shape<State>>,
setOptions: (options: $Shape<Options>) => Promise<$Shape<State>>,
};
So to return a reference to this instance we first need to change PopperWrapper.js
so that it actually returns what createPopper
returns.
return Popper.createPopper(reference, popper, options);
Then we need to update the createPopper
call so that it returns an IJSObjectReference
instead of void.
public async Task<Instance> CreatePopperAsync(ElementReference reference, ElementReference popper, Options options)
{
var objRef = DotNetObjectReference.Create(options);
var popperWrapper = await jSRuntime.InvokeAsync<IJSInProcessObjectReference>("import", "./js/PopperWrapper.js");
var jSInstance = await popperWrapper.InvokeAsync<IJSObjectReference>("createPopper", reference, popper, options, objRef);
return new Instance(jSInstance, objRef, popperWrapper);
}
We have also updated the import invocation so that it returns an IJSInProcessObjectReference
instead. This is simply a variant of IJSObjectReference
that can also call methods synchronously. This is necessary because the Instance
type has a property that we will need to access synchronously, but more about that in just a bit.
createPopper
now returns an IJSObjectReference
. We can call invocations directly on this. But to enforce the types that we have previously defined, let's encapsulate them in a new class. We call this class Instance
which takes the Instance
JS reference, the options
argument .NET reference, and the PopperWrapper
since we will need to invoke some new wrapper calls from the Instance
class.
We first have some functions that are easy to implement: destroy
and forceUpdate
functions. They are easy to create since they take no complex arguments nor return anything. We implement the constructor for the class and these methods like so.
public class Instance : IDisposable
{
private readonly IJSObjectReference jSInstance;
private readonly DotNetObjectReference<Options> objRef;
private readonly IJSInProcessObjectReference popperWrapper;
public Instance(IJSObjectReference jSInstance, DotNetObjectReference<Options> objRef, IJSInProcessObjectReference popperWrapper)
{
this.jSInstance = jSInstance;
this.objRef = objRef;
this.popperWrapper = popperWrapper;
}
public async Task ForceUpdate() => await jSInstance.InvokeVoidAsync("forceUpdate");
public async Task Destroy() => await jSInstance.InvokeVoidAsync("destroy");
public void Dispose()
{
objRef?.Dispose();
}
}
Notice that we have made Instance
implement IDisposable
. We have done this so that it will dispose the .NET object reference to the options argument when it is disposed itself. This way we move the object reference into being specific for each Instance
instead of being shared among all of them. We have removed the objRef
property from the Popper
class so its lifetime is only handled in the Instance
class. We save all the arguments given to the constructor in readonly
properties and use them when we want to invoke a function. We simply call InvokeVoidAsync
on the IJSObjectReference
with a function name from the JS Instance
object. This is easy because we have no arguments and no return values.
Adding the rest of the properties/methods to Instance
Next, let's implement the state
property. We want to keep this as a property to make it reflect the original structure as much as possible. This is where the popperWrapper
that is an IJSInProcessObjectReference
comes into play. We add the following property to the Instance
class.
public State State
{
get { return popperWrapper.Invoke<State>("getStateOfInstance", jSInstance); }
}
Now we can Invoke
functions synchronously. We make this call to our PopperWrapper
because we need to adjust which fields are included in the state (as we have done previously) to escape cyclic references in the JS object when it is returned. But if the response was not cyclic then we could just have called jSInstance.Invoke<State>("state.valueOf")
given that jSInstance
is an IJSInProcessObjectReference
. Let's look at how we implement the getStateOfInstance
wrapper. We simply add the following to our existing PopperWrapper.js
file.
export function getStateOfInstance(instance) {
var state = instance.state;
return {
placement: state.placement
}
}
We see that we can get the state property directly from the parsed instance
. Then we return a new object that only has the placement
property included as this is the only thing we have included in our State
class currently.
Now, we will implement the update
method. It also returns a State
object but according to the specifications for the instance
type it's actually a promise of a State
. In the C# end we simply add the following method to the Instance
class.
public async Task<State> Update() => await popperWrapper.InvokeAsync<State>("updateOnInstance", jSInstance);
This also calls on the popperWrapper
but uses the asynchronous invocation as previously. IJSInProcessObjectReference
can do both synchronous and asynchronous calls since it extends the IJSObjectReference
interface. Now let's go to the wrapper method.
export function updateOnInstance(instance) {
return instance.update().then(state => ({ placement: state.placement }));
}
It first calls the update
function on the instance. This returns a promise. This is a special object used for asynchronous calls. You can call multiple methods on it like with the builder pattern to react to different outcomes of the asynchronous call. Among these are the then
method. This takes a method that it calls with the state object once the asynchronous call is finished. We use this to return a new object with a placement field once the call is finished.
The last method that we need to wrap is setOptions
. This also needs a wrapper since it also returns a State
object. It also needs the wrapper because we need to set the onFirstUpdate
field in the options object as we did when we constructed the JS Popper
.
public async Task<State> SetOptions(Options options) => await popperWrapper.InvokeAsync<State>("setOptionsOnInstance", jSInstance, options, objRef);
We parse the jSInstance
, but also the options
and objRef
as we did in the constructor. In the wrapper we do something very similar to what we have done previously in the constructor.
export function setOptionsOnInstance(instance, options, objRef) {
options.onFirstUpdate = (state) => {
const stateCopy = {
placement: state.placement
}
objRef.invokeMethodAsync('CallOnFirstUpdate', stateCopy)
};
return instance.setOptions(options).then(state => ({ placement: state.placement }) );
}
Notice that the setOptions
function also returns a promise of a State
object. So we have to do the same as in the update
function when returning.
Demo
Now let's test some of the functions. We setup a page to test it like so:
@page "/"
@inject Popper Popper
<br />
<span id="reference" @ref=reference style="background-color:blue;">Reference</span>
<span id="popper" @ref=popper style="background-color:red;width:100px;">@state?.Placement.ToString()</span>
<br />
<br />
@foreach (Placement placement in Enum.GetValues(typeof(Placement)))
{
<button @onclick="()=>SetPlacement(placement)" >@placement.ToString()</button>
}
@code {
protected ElementReference reference;
protected ElementReference popper;
protected Instance Instance;
protected State state;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Instance = await Popper.CreatePopperAsync(reference, popper, new());
}
}
public async Task SetPlacement(Placement placement)
{
Options options = new()
{
Placement = placement
};
state = await Instance.SetOptions(options);
}
}
We first define two spans as we have done previously. These act as our reference
element and the popper
element. We bind these using the @ref
tag. The popper
span displays the Placement
of the popper
which we access from the state
. We set the state
later. We create the Popper
Instance
in the OnAfterRenderAsync
method which is the last part of the Blazor render-cycle. Notice that we parse an empty Options
using the new()
syntax since the type is implicit.
Next, we have added buttons for all the different values of the Placement
Enum
. Each button has a @onclick
tag defined that calls the SetPlacement
method. This method calls the Instance
method SetOptions
with the respective Placement
in a new Options
object. Then we set the state
to the returned State
which updates the text the in the popper.
All the code for this project and this example can be found at: github.com/elmahio-blog/PopperBlazor.
Finally, we also have a small video that shows the final result:
Conclusion
First, we have updated a Blazor WASM project from .NET Core 3.1 to .NET 5.0. This enabled us to use JavaScript isolation in place of our existing way of referencing our JS wrapper. Next, we also used IJSObjectReference
s to invoke functions on JS Objects from an external library. In the end, we demoed these updates to the project in a small example. We might continue this series with more articles in the future. The next step could be making unit tests for a Blazor library using bUnit. If you have any questions for this article or feedback then feel free to reach out.