Blazor Rich Text Editor using CKEditor

I'll start with a disclaimer. I just started working with Blazor and I'm still getting my bearings. I needed to add a rich text editor to a form I was working on, and found the existing options lacking. Currently, all I could find were Blazored Text Editor and Syncfusion's Blazor Rich Text Editor. The Blazored Text Editor uses Quill JS, which I don't really care for. Syncfusion's version seems fine, but it's license-encumbered. My preferred rich text editor is CKEditor 5, so I set about to create a component using that, which meant coming to terms with JS interop.

Blazor is incredibly simple to work with... until it isn't. Integrating random JS libraries is a tall order until you understand how JS interop works, and the docs are pretty minimal here. However, once I got my head around it, it ended up being surprisingly easy. That said, this is literally my first go at something like this, so I would very much welcome any comments or suggestions for improvement. With that out of the way, let's dig in.

InputRichText.razor

@inherits InputTextArea
@inject IJSRuntime JSRuntime

<textarea @attributes="AdditionalAttributes"  
       id="@Id"
       class="@CssClass"
       value="@CurrentValue"></textarea>

@code { 
    string _id;
    [Parameter]
    public string Id
    {
        get => _id ?? $"CKEditor_{uid}";
        set => _id = value;
    }

    readonly string uid = Guid.NewGuid().ToString().ToLower().Replace("-", "");

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            await JSRuntime.InvokeVoidAsync("CKEditorInterop.init", Id, DotNetObjectReference.Create(this));

        await base.OnAfterRenderAsync(firstRender);
    }

    [JSInvokable]
    public Task EditorDataChanged(string data)
    {
        CurrentValue = data;
        StateHasChanged();
        return Task.CompletedTask;
    }

    protected override void Dispose(bool disposing)
    {
        JSRuntime.InvokeVoidAsync("CKEditorInterop.destroy", Id);
        base.Dispose(disposing);
    }
}

CKEditorInterop.js

window.CKEditorInterop = (() => {  
    const editors = { };

    return {
        init(id, dotNetReference) {
            ClassicEditor
                .create(document.getElementById(id))
                .then(editor => {
                    editors[id] = editor;
                    editor.model.document.on('change:data', () => {
                        let data = editor.getData();

                        const el = document.createElement('div');
                        el.innerHTML = data;
                        if (el.innerText.trim() == '')
                            data = null;

                        dotNetReference.invokeMethodAsync('EditorDataChanged', data);
                    });
                })
                .catch(error => console.error(error));
        },
        destroy(id) {
            editors[id].destroy()
                .then(() => delete editors[id])
                .catch(error => console.log(error));
        }
    };
})();

With those two files in place, add the following to your _Host.cshtml:

    <script src="https://cdn.ckeditor.com/ckeditor5/16.0.0/classic/ckeditor.js"></script>
    <script src="~/js/CKEditorInterop.js"></script>
    <script src="_framework/blazor.server.js"></script>
</body>  
comments powered by Disqus