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

Using JS Object References in Blazor WASM to wrap JS libraries

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 DiscriptionsAttributes. In the end, we also added the capability to call C# delegates from Popper. This resulted in the following:

Example of popper that is set to the right of reference object.

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

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website