4 min read
Tim Purdum

Using ObjectReferences to Embed a JavaScript Text Editor in Blazor

4 min read
Tim Purdum

In Modern Web Development in C# we discussed the Asp.NET Core Blazor framework, which allows .NET developers to use C# to build rich web applications, with modes for rendering both on a server and on the client browser. While the promise of such a framework is writing web applications without JavaScript, the reality is that the JavaScript ecosystem is larger, more mature, and has direct access to the HTML DOM. Blazor developers need to know how to tap into this ecosystem, when necessary, to manually update or listen for DOM events, or to add advanced components from a JavaScript library. Using ObjectReferences, we can communicate in two directions between JS and C# objects.

In this post, we are going to explore hooking up a JavaScript-designed rich text editor as a Blazor Component. We will use Quill as our editor. As we proceed, you will learn how to create IJSObjectReference objects to call into JavaScript functions, and DotNetObjectReference objects to call back to C# code.

You can begin with a dotnet new blazor wasm blank template. The full code for this post can be browsed at https://github.com/dymaptic/dy-blazor-object-refs.

Adding Quill to the Project

First, we will add a few links in our wwwroot/index.html root web page. In the head tag, let’s add the following css link.

<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">

And at the bottom of the body tag, add the following JavaScript source.

<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>

Creating a JavaScript Module and Function

Now, create a new JavaScript file in your wwwroot directory, such as jsInterop.js. Add the following function, where we introduce our first usage of an ObjectReference.

// export makes this function available by importing the file as a module
    export function getQuill(containerId, dotNetRef, options) {
        let quill = new Quill(`#${containerId}`, options);
        let Delta = Quill.import('delta');
        let change = new Delta();
        // add an event listener to 'text-change'
        quill.on('text-change', (delta) => {
            change = change.compose(delta);
            // pass the change delta back to .NET
            dotNetRef.invokeMethodAsync('JsTextChanged', change);
        })
    
        // returns the object to .NET so that you can call functions on that object.
        return quill;
    }

You can see this basically sets up a new Quill editor, attaches an event handler, and returns the editor. Note the dotNetRef object. This is a DotNetObjectReference from C#, which exposes one function, invokeMethodAsync, which is used to call back into .NET.

Creating a Razor Component

Now, let’s create a Razor Component file called Quill.razor, where we will create both the .NET ObjectReference, and the JavaScript ObjectReference.

@inject IJSRuntime JsRuntime

        <div id="quill-container"></div>
        
        @code
        {
            [Parameter]
            public string? Theme { get; set; }
        
            [Parameter]
            public string? Placeholder { get; set; }
        
            // allows calling code to listen for text changed
            [Parameter]
            public EventCallback<object> TextChanged { get; set; }
        
            // this is the method called in JavaScript by `dotNetRef.invokeMethodAsync`
            [JSInvokable]
            public async Task JsTextChanged(object delta)
            {
                await TextChanged.InvokeAsync(delta);
            }
        
            // this method works as a "pass-through" to call into a JavaScript function from xona
            public async Task<string> GetText()
            {
                return await _quill!.InvokeAsync<string>("getText");
            }
        
            public async Task Enable(bool enabled)
            {
                await _quill!.InvokeVoidAsync("enable", enabled);
            }
        
            protected override async Task OnAfterRenderAsync(bool firstRender)
            {
                if (firstRender)
                {
                    // gets a reference to the file `jsInterop.js` as a JavaScript module
                    IJSObjectReference module = await JsRuntime.InvokeAsync<IJSObjectReference>("import", "./jsInterop.js");
                    Dictionary<string, object?> options = new();
                    if (Theme is not null)
                    {
                        options["theme"] = Theme;
                    }
                    if (Placeholder is not null)
                    {
                        options["placeholder"] = Placeholder;
                    }
                    // calls our JavaScript function and gets a reference to the Quill
                    _quill = await module.InvokeAsync<IJSObjectReference>("getQuill", "quill-container",
                        DotNetReference, options);
                }
            }
        
            private IJSObjectReference? _quill;
            private DotNetObjectReference<Quill> DotNetReference => DotNetObjectReference.Create(this);
        }

Notice that there were two IJSObjectReference objects created here, one for the module, which points at our JavaScript file, and then one for the _quill editor itself. While you can create top-level JavaScript functions, it is normally recommended to keep them all within modules.

The IJSObjectReference allows you to dynamically call any function on the type that you referenced. According to the Quill API docs, there are a lot of functions we could invoke and expose here. We chose to implement two, getText, to return the plain text of the editor, and enable to toggle the editor on/off.

The DotNetObjectReference<Quill> refers to this Razor Component. This is passed into our JavaScript function to be used for the text-changecallback.

Consuming our new Quill Component

Here is our Pages/Index.razor file, with all new code to inject and talk to our Quill Component.

@page "/"
          @using System.Text.Json
          
          &lt;PageTitle&gt;Index&lt;/PageTitle&gt;
          
          &lt;h1&gt;Quill Editor&lt;/h1&gt;
          &lt;button @onclick="ToggleQuill"&gt;@(_isEnabled ? "Disable Editor" : "Enable Editor")&lt;/button&gt;
          
          @* Here is our Quill component being created in the page *@
          &lt;Quill @ref="_quill"
                 Theme="snow"
                 Placeholder="Write a blog post..."
                 TextChanged="OnTextChanged" /&gt;
          
          &lt;h2&gt;Delta Content&lt;/h2&gt;
          
          &lt;div style="max-height: 400px; overflow-y: scroll"&gt;
              @((MarkupString)_deltaJson)
          &lt;/div&gt;
          
          &lt;button @onclick="GetText"&gt;Get Text&lt;/button&gt;
          @if (!string.IsNullOrWhiteSpace(_textContent))
          {
              &lt;h2&gt;Raw Content&lt;/h2&gt;
              &lt;div&gt;@_textContent&lt;/div&gt;
          }
          
          @code
          {
              private Quill _quill = default!;
          
              // event handler for text changed
              private async Task OnTextChanged(object delta)
              {
                  // a little editing to make the json look good in html
                  _deltaJson = JsonSerializer.Serialize(delta, _jsonOptions)
                      .Replace(Environment.NewLine, "&lt;br&gt;")
                      .Replace(" ", "&amp;nbsp;&amp;nbsp;");
              }
          
              private async Task GetText()
              {
                  // calls to get the current plain text
                  _textContent = await _quill.GetText();
              }
          
              private async Task ToggleQuill()
              {
                  // toggles the editor on/off
                  _isEnabled = !_isEnabled;
                  await _quill.Enable(_isEnabled);
              }
          
              private string _deltaJson = string.Empty;
              private string _textContent = string.Empty;
              private bool _isEnabled = true;
          
              private readonly JsonSerializerOptions _jsonOptions = new()
              {
                  WriteIndented = true
              };
          }

Go ahead and run your project. You should see something like the screen shot below.

quill sample

Review of ObjectReferences

IJSObjectReference

Instantiation

  • Create as an ObjectReference to a JS module by calling JsRuntime.InvokeAsync("import", "nameOfModuleFile.js").
  • Create as an ObjectReference to any JS object by calling module.InvokeAsync("customExportFunction") in reference to a function you defined that returns the object.

Calling Functions

  • All exported (public) functions on the object can be called via .NET with ref.InvokeAsync or ref.InvokeVoidAsync.

Limitations

  • Return types from InvokeAsync must be serializable. Note that HTMLElements will often fail to serialize because of circular references.
  • You cannot access an object property via IJSObjectReference. If you want to get an object as a data set with properties, you should retrieve it as a C# object, dynamic, or matching strongly typed class, record, or struct, instead of as an IJSObjectReference.

DotNetObjectReference

Instantiation

  • Create as an ObjectReference to the existing C# class or Razor Component with DotNetObjectReference.Create(this). You could also pass a different .NET object reference in place of this.
  • You must pass the DotNetObjectReference as an argument to a JS function via module.InvokeAsync or module.InvokeVoidAsync before calling.

Calling Methods

  • Methods called by JavaScript must be decorated with the [JSInvokable] attribute.
  • In JavaScript, invoke the method with dotNetRef.invokeMethodAsync("methodName", arguments...)

Limitations

  • Like with IJSObjectReference, there is no way to reference class properties, only methods.
  • Arguments must be JSON serializable. JSInvokable method parameters should be typed to match the incoming JSON with strongly typed objects, generic object, or dynamic.

Conclusion

I hope this overview of the communication between JavaScript and C# in Blazor was helpful to you. Please share this post and let me know if you have questions via the comment form below, or by contacting tim.purdum@dymaptic.com. If you are looking for a well-wrapped and feature-rich GIS and Mapping Component library, check out GeoBlazor.

Try GeoBlazer

Share

Related articles

Using ESBuild with Blazor

Using ESBuild with Blazor

How to Bundle JavaScript packages and Compile TypeScript for your Blazor project With Blazor, .NET developers can create fully-featured clie...
Creating ArcGIS Web Mapping Apps in C# Using Blazor

Creating ArcGIS Web Mapping Apps in C# Using Blazor

In my previous post, I showed you how to get up and running with ASP.NET Blazor web development. In this post, I will show you how to use th...
Let the Sunshine In: Finding Your Daylight with GeoBlazor

Let the Sunshine In: Finding Your Daylight with GeoBlazor

Here in the Northern Hemisphere, we are rapidly approaching the Winter Solstice, also known as the shortest day of the year. Yet sunshine is...