Building a Reusable Blazor Component for Summernote
09 Mar 2026 #csharp #todayilearnedIn another adventure with Blazor…
While migrating a legacy app, I had to “componentize” an HTML editor. It used Summernote, “a super simple WYSIWYG editor on Bootstrap”.
The fun part was learning JavaScript interop: calling JavaScript from .NET and viceversa.
After some Googling and sneaking into abandoned GitHub repos, here’s what I came up with:
The component
In Summernote.razor:
@using Microsoft.AspNetCore.Components.Sections
<HeadContent>
<link rel="stylesheet" href="css/summernote.css" />
</HeadContent>
<div id="@_id">@_markup</div>
<SectionContent SectionName="scripts">
<script src="js/summernote.js" type="text/javascript"></script>
</SectionContent>
In Summernote.razor.cs:
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace MyCoolComponents.HtmlEditor;
public partial class Summernote : IDisposable
{
private readonly string _id = $"summernote_{Guid.NewGuid()}";
private IJSObjectReference? _module;
private DotNetObjectReference<Summernote>? _dotnetRef;
private bool _editorInitialized = false;
private MarkupString _markup = new MarkupString();
[Parameter]
public string Value
{
get
{
return _markup.ToString();
}
set
{
_markup = (MarkupString)value;
}
}
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
[Inject]
public IJSRuntime JsRuntime { get; set; } = default!;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_editorInitialized)
{
_dotnetRef = DotNetObjectReference.Create(this);
_module = await JsRuntime.InvokeAsync<IJSObjectReference>("import", "./PutYourPathHere/Summernote.razor.js");
await _module.InvokeVoidAsync("edit", _id, _dotnetRef, nameof(OnTextChange));
_editorInitialized = true;
}
}
[JSInvokable]
public async Task<bool> OnTextChange(string editorText)
{
_markup = (MarkupString)editorText;
await ValueChanged.InvokeAsync(editorText);
return await Task.FromResult(true);
}
public void Dispose()
{
_dotnetRef?.Dispose();
}
}
Here’s where the magic happens.
After rendering the component, it calls a JavaScript function that initializes the Summernote editor. Then, it registers a callback that calls a .NET function every time the editor changes to update the component state.
Notice it stores content in a MarkupString but it binds as a string.
How to use it
In Summernote.razor.js:
export function edit(id, dotnetRef, callback) {
let snid = '#' + id;
$(snid).summernote({
callbacks: {
onChange: function (contents, $editable) {
dotnetRef.invokeMethodAsync(callback, contents);
}
}
});
}
And here’s a sample form using the editor:
@using MyCoolComponents.HtmlEditor
<EditForm Model="MyCoolRequest" OnValidSubmit="OnValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="row">
<div class="form-group col-sm-9">
<label for="content" class="form-label">Content</label>
<Summernote @bind-Value="Annotation.Content" />
@* ^^^^^ *@
@* Look, Ma! It works! *@
<ValidationMessage For="() => Annotation.Content" class="invalid-feedback" />
</div>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</EditForm>
This piece of code is provided “as is”, without warranty of any kind…Blah, blah, blah. Use under your own risk. Steal it.
If this helped you, you’ll love Street-Smart Coding—30 proven lessons to help you level up your coding journey.