Blazor Quill Component Example

In this new post, I show you how to create a Blazor component for Quill. With Quill you can add to your application a nice and easy to use web editor.

As a point of interest, I use interaction with JavaScript. You can easily create re-usable custom controls for your Blazor applications even if they contain assets such as JavaScript.

First, you can download the source code from GitHub or download my Nuget package.

Then, for more documentation, example and components about Blazor, here same links in this blog:

What is Quill?

Quill is a free, open source WYSIWYG editor built for the modern web. With its modular architecture and expressive API, it is completely customizable to fit any need.

Quill Rich Text Editor example - Create a Blazor component for Quill
Quill Rich Text Editor example

Features

So, Blazor Quill Component is a custom reusable control that allows us to easily consume Quill and place multiple instances of it on a single page in our Blazor application. Also, it will allow us to implement the following features:

  • Exports editor contents in TextHTML, and Quill’s native Delta format
  • Allows initial content to be set declaratively in the control or programmatically
  • Provides a read only mode, suitable for displaying Quill’s native Delta format
  • Allows custom toolbars to be defined
  • Allows custom themes
  • Has an inline editor mode that also allows custom toolbars

Create the component

So, I have to create a new Razor Class Library. In Visual Studio 2019, Add a new project and select this option from the list and then follow the wizard. My project is called PSC.Blazor.Components.Quill but fell free to use the name you like.

Add a new project for the Blazor component - Create a Blazor component for Quill
Add a new project for the Blazor component

Now, the component has only 3 files:

  • BlazorQuill.js under wwwroot
  • QuillInterop.cs
  • QuillEditor.razor

Before anything else, add in _Imports.razor the following line to use JSInterop:

@using Microsoft.JSInterop

Add BlazorQuill.js

So, under the wwwroot we can add static files like JavaScript, images, CSS and so on. Under this folder, I’m going to create the BlazorQuill.js. This file contains QuillFunctions function to invoke changes to the QuillJs. The code for this file is the following

(function () {
	window.QuillFunctions = {
		createQuill: function (
			quillElement, toolBar, readOnly,
			placeholder, theme, debugLevel) {

			var options = {
				debug: debugLevel,
				modules: {
					toolbar: toolBar
				},
				placeholder: placeholder,
				readOnly: readOnly,
				theme: theme
			};

			new Quill(quillElement, options);
		},
		getQuillContent: function (quillElement) {
			return JSON.stringify(quillElement.__quill.getContents());
		},
		getQuillText: function (quillElement) {
			return quillElement.__quill.getText();
		},
		getQuillHTML: function (quillElement) {
			return quillElement.__quill.root.innerHTML;
		},
		loadQuillContent: function (quillElement, quillContent) {
			content = JSON.parse(quillContent);
			return quillElement.__quill.setContents(content, 'api');
		},
		enableQuillEditor: function (quillElement, mode) {
			quillElement.__quill.enable(mode);
		}
	};
})();

The functions are:

  • createQuill: this function is responsible to apply QuillJS to the elements in the page.
  • getQuillContent returns the content in the QuillJS in Delta format
  • getQuillText: this returns only the text from the editor
  • getQuillHTML returns the HTML from the editor
  • loadQuillContent: this function imports a document in Delta format in the editor
  • enableQuillEditor

QuillInterop

So, how does our application know how to call the JavaScript? I have to create something that sits in the middle between JavaScript and Blazor. QuillInterop is the connection between them based on JSInterop.

using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Threading.Tasks;

namespace PSC.Blazor.Components.Quill
{
	public static class QuillInterop
	{
		private const string strCreateQuill = "QuillFunctions.createQuill";
		private const string strGetText = "QuillFunctions.getQuillText";
		private const string strGetHTML = "QuillFunctions.getQuillHTML";
		private const string strGetContent = "QuillFunctions.getQuillContent";
		private const string strLoadQuillContent = "QuillFunctions.loadQuillContent";
		private const string strEnableQuillEditor = "QuillFunctions.enableQuillEditor";

		internal static ValueTask<object> CreateQuill(
			IJSRuntime jsRuntime,
			ElementReference quillElement,
			ElementReference toolbar,
			bool readOnly,
			string placeholder,
			string theme,
			string debugLevel)
		{
			return jsRuntime.InvokeAsync<object>(
				strCreateQuill,
				quillElement, toolbar, readOnly,
				placeholder, theme, debugLevel);
		}

		internal static ValueTask<string> GetText(
			IJSRuntime jsRuntime,
			ElementReference quillElement)
		{
			return jsRuntime.InvokeAsync<string>(
				strGetText,
				quillElement);
		}

		internal static ValueTask<string> GetHTML(
			IJSRuntime jsRuntime,
			ElementReference quillElement)
		{
			return jsRuntime.InvokeAsync<string>(
				strGetHTML,
				quillElement);
		}

		internal static ValueTask<string> GetContent(
			IJSRuntime jsRuntime,
			ElementReference quillElement)
		{
			return jsRuntime.InvokeAsync<string>(
				strGetContent,
				quillElement);
		}

		internal static ValueTask<object> LoadQuillContent(
			IJSRuntime jsRuntime,
			ElementReference quillElement,
			string Content)
		{
			return jsRuntime.InvokeAsync<object>(
				strLoadQuillContent,
				quillElement, Content);
		}

		internal static ValueTask<object> EnableQuillEditor(
			IJSRuntime jsRuntime,
			ElementReference quillElement,
			bool mode)
		{
			return jsRuntime.InvokeAsync<object>(
				strEnableQuillEditor,
				quillElement, mode);
		}
	}
}

QuillEditor.razor

Now, the last (but not least) part of the component, the Razor component. So, here is the magic happens. I defined 2 div for the toolbar and the QuillJs element. Then, in the code I define some parameters and the function to expose like GetText() or GetContent(). Those functions are invoking the QuillInterop and get data from or set QuillJs via Javascript. EditorContent and ToolbarContent are the tags I can use in the page of my application to pass the text I want to display and the toolbar I want to use.

@inject IJSRuntime JSRuntime

<div @ref="@ToolBar">
	@ToolbarContent
</div>

<div @ref="@QuillElement">
	@EditorContent
</div>

@code {
	[Parameter] public RenderFragment EditorContent { get; set; }
	[Parameter] public RenderFragment ToolbarContent { get; set; }
	[Parameter] public bool ReadOnly { get; set; } = false;
	[Parameter] public string Placeholder { get; set; } = "Insert text here...";
	[Parameter] public string Theme { get; set; } = "snow";
	[Parameter] public string DebugLevel { get; set; } = "info";

	private ElementReference QuillElement;
	private ElementReference ToolBar;

	protected override async Task
		OnAfterRenderAsync(bool firstRender)
	{
		if (firstRender)
		{
			await QuillInterop.CreateQuill(
				JSRuntime,
				QuillElement,
				ToolBar,
				ReadOnly,
				Placeholder,
				Theme,
				DebugLevel);
		}
	}

	public async Task<string> GetText()
	{
		return await QuillInterop.GetText(
			JSRuntime, QuillElement);
	}

	public async Task<string> GetHTML()
	{
		return await QuillInterop.GetHTML(
			JSRuntime, QuillElement);
	}

	public async Task<string> GetContent()
	{
		return await QuillInterop.GetContent(
			JSRuntime, QuillElement);
	}

	public async Task LoadContent(string Content)
	{
		var QuillDelta =
			await QuillInterop.LoadQuillContent(
				JSRuntime, QuillElement, Content);
	}

	public async Task EnableEditor(bool mode)
	{
		var QuillDelta =
			await QuillInterop.EnableQuillEditor(
				JSRuntime, QuillElement, mode);
	}
}

Use the component in an example

Now, I have the component and it is time to test it. In Visual Studio, add a new Blazor WebApplication project in the solution. Then, add the component as dependencies clicking the right-click on Dependencies in the project and then Add project Reference. So, in the _Imports.razor add the reference to this component adding the following line

@using PSC.Blazor.Components.Quill

Now, the tricky part. We have to add CSS and JavaScript in the project. Under wwwroot, you have index.html. Open it. I want to use the themes for QuillJs. I have to add the references for those in the head of the page and there are coming from a CDN. You can save them locally and change the href. Now, the same for the QuillJs Javascript. To add the JavaScript for my component, I have to add a _content reference.

Here you have the full index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>PSC.BlazorQuillExample</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="PSC.BlazorQuillExample.styles.css" rel="stylesheet" />

    <!-- Quill stylesheet -->
    <link href="//cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
    <link href="//cdn.quilljs.com/1.3.6/quill.bubble.css" rel="stylesheet">
</head>

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>

    <!-- Quill library -->
    <script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
    <script src="_content/PSC.Blazor.Components.Quill/BlazorQuill.js"></script>
</body>
</html>

Change the Index.razor

Now, if you replace all the content in the Index.razor with the following code

@page "/"
@using QuillControl
<QuillEditor @ref="@QuillHtml">
    <ToolbarContent>
    </ToolbarContent>
    <EditorContent>
        <h1>Hello World!</h1>
    </EditorContent>
</QuillEditor>

@code {    
}

The result is only a simple text like in the following image.

First look at the component in the application
First look at the component in the application

Ok, it is working but we want more. at least the toolbar. So, in the QuillEditor add a ToolbarContent tag and you can see all the options for customizing the Toolbar in the documentation. Then, I want to add some custom text in the editor for the user as placeholder. For that, there is the EditorContent tag that accepts HTML code.

<QuillEditor @ref="@QuillHtml">
	<ToolbarContent>
		<select class="ql-header">
			<option selected=""></option>
			<option value="1"></option>
			<option value="2"></option>
			<option value="3"></option>
			<option value="4"></option>
			<option value="5"></option>
		</select>
		<span class="ql-formats">
			<button class="ql-bold"></button>
			<button class="ql-italic"></button>
			<button class="ql-underline"></button>
			<button class="ql-strike"></button>
		</span>
		<span class="ql-formats">
			<select class="ql-color"></select>
			<select class="ql-background"></select>
		</span>
		<span class="ql-formats">
			<button class="ql-list" value="ordered"></button>
			<button class="ql-list" value="bullet"></button>
		</span>
		<span class="ql-formats">
			<button class="ql-link"></button>
		</span>
	</ToolbarContent>
	<EditorContent>
		<h4>This Toolbar works with HTML</h4>
		<a href="https://www.puresourcecode.com/">
			PureSourceCode
		</a>
	</EditorContent>
</QuillEditor>

Programmatically retrieving content

In the @code section of the page, add the following code:

QuillEditor QuillHtml;
string QuillHTMLContent;

public async void GetHTML()
{
	QuillHTMLContent = await this.QuillHtml.GetHTML();
	StateHasChanged();
}

and in the markup section I have the following code

<button class="btn btn-primary"
		@onclick="GetHTML">
	Get HTML
</button>
<div style="height: 5px; width: 100%;"></div>
<div style="width: 100%; background-color: #dcdcdc;">
	@((MarkupString)QuillHTMLContent)
	@QuillHTMLContent
</div>

So, when I run the application, I can click the Get HTML button to display the contents of the editor as HTML on the page as well as display the raw HTML. This happens because the code invokes the QuillInterop and it invokes the JavaScript that replies to the QuillInterop the HTML code and the QuillInterop passes the value to the UI.

Native content

While it is possible to store and retrieve HTML content from the editor control, HTML is unable to capture all formatting that the editor control supports, for example centering text.

To support all the options, you will need to export and load content in the Quill control’s native Delta format.

Add the following markup code:

<QuillEditor @ref="@QuillNative"
				Placeholder="Enter non HTML format like centering...">
	<ToolbarContent>
		<span class="ql-formats">
			<select class="ql-font">
				<option selected=""></option>
				<option value="serif"></option>
				<option value="monospace"></option>
			</select>
			<select class="ql-size">
				<option value="small"></option>
				<option selected=""></option>
				<option value="large"></option>
				<option value="huge"></option>
			</select>
		</span>
		<span class="ql-formats">
			<button class="ql-bold"></button>
			<button class="ql-italic"></button>
			<button class="ql-underline"></button>
			<button class="ql-strike"></button>
		</span>
		<span class="ql-formats">
			<select class="ql-color"></select>
			<select class="ql-background"></select>
		</span>
		<span class="ql-formats">
			<button class="ql-list" value="ordered"></button>
			<button class="ql-list" value="bullet"></button>
			<button class="ql-indent" value="-1"></button>
			<button class="ql-indent" value="+1"></button>
			<select class="ql-align">
				<option selected=""></option>
				<option value="center"></option>
				<option value="right"></option>
				<option value="justify"></option>
			</select>
		</span>
		<span class="ql-formats">
			<button class="ql-link"></button>
		</span>
		<span class="ql-formats">
			<button class="ql-image"></button>
		</span>
	</ToolbarContent>
</QuillEditor>

<button class="btn btn-primary"
		@onclick="GetContent">
	Get Content
</button>
<button class="btn btn-primary"
		@onclick="LoadContent">
	Load Content
</button>
<div>
	@QuillContent
</div>

and in the @code section add the following code

QuillEditor QuillNative;
string QuillContent;

public async void GetContent()
{
	QuillContent = await this.QuillNative.GetContent();
	StateHasChanged();
}

public async void LoadContent()
{
	await this.QuillNative.LoadContent(QuillContent);
	StateHasChanged();
}

When I run the application I see that there is custom placeholder text that was set using the Placeholder property. Also, I see that there are toolbar options such as centering and indenting that are not supported by HTML.

Quill native when you click Get Content - Content in Delta format
Quill native when you click Get Content – Content in Delta format

Display read-only and Inline editing

HTML content can be displayed on any page when using the Blazor (MarkupString) tag, however, the Quill native Delta format requires the Quill editor to display.

When you want the content displayed in a read only format, you can set the ReadOnly property on the control, and set the theme to bubble Add the following markup code:

<QuillEditor @ref="@QuillReadOnly"
				ReadOnly="true"
				Theme="bubble"
				DebugLevel="log">
	<ToolbarContent>
		<select class="ql-header">
			<option selected=""></option>
			<option value="1"></option>
			<option value="2"></option>
			<option value="3"></option>
			<option value="4"></option>
			<option value="5"></option>
		</select>
		<span class="ql-formats">
			<button class="ql-bold"></button>
			<button class="ql-italic"></button>
			<button class="ql-underline"></button>
			<button class="ql-strike"></button>
		</span>
		<span class="ql-formats">
			<select class="ql-color"></select>
			<select class="ql-background"></select>
		</span>
		<span class="ql-formats">
			<button class="ql-list" value="ordered"></button>
			<button class="ql-list" value="bullet"></button>
		</span>
		<span class="ql-formats">
			<button class="ql-link"></button>
		</span>
	</ToolbarContent>
	<EditorContent>
		@((MarkupString)@QuillReadOnlyContent)
	</EditorContent>
</QuillEditor>

<button class="btn btn-info"
		@onclick="ToggleQuillEditor">
	Toggle Editor
</button>

Add the following code to the @code section:

QuillEditor QuillReadOnly;
string QuillReadOnlyContent = @"<span><b>Read Only</b> <u>Content</u></span>";
bool mode = false;

async Task ToggleQuillEditor()
{
	mode = !mode;
	await this.QuillReadOnly.EnableEditor(mode);
}

Now, when I run the application, I see that the content (this can be TextHTML, or Quill native Delta content), is displayed in read only mode. If I click the Toggle Editor button, the content becomes editable. Clicking on content, brings up the formatting toolbar defined in the markup for the control.

Quill read-only with Inline editor
Quill read-only with Inline editor

Fix for images

The last thing is a quick fix for the images. If you want to add big images in the editor, you will receive an error from Blazor. For that, I added in the component Quill Blot Formatter.

QuillJs module to format document blots. Heavily inspired by quill-image-resize-module. Out of the box supports resizing and realigning images and iframe videos, but can be easily extended using BlotSpec and Action.

So, if you want to add images in the editor, you have to add in the index.html this line

<script src="_content/PSC.Blazor.Components.Quill/quill-blot-formatter.min.js"></script>

Bonus content: get your Blazor GitHub project to properly show C#

In GitHub, many Blazor projects will display the language as JavaScript when it should be C#. Here is how to fix that. So, add this line to your .gitattributes file:

*.razor linguist-language=C#

By Enrico

My greatest passion is technology. I am interested in multiple fields and I have a lot of experience in software design and development. I started professional development when I was 6 years. Today I am a strong full-stack .NET developer (C#, Xamarin, Azure)

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.