- Published on
QuillJS & Alpine
- Author
-
-
- Name
- owls
- Mastodon
- @owls@yshi.org
-
I'm working on an app that requires some specific features in a rich text editor widget. I picked Quill, and I'll have more to say on that (probably), but I wanted to make some notes on a specific Quill & AlpineJS problem I ran into.
To keep things simple, it would be best if the editor would accept and produce HTML with style=
attributes for colours and things. Good ol' [HTMLPurifier](https://htmlpurifier.org] can keep user-supplied markup under control.
The app will use AlpineJS and Livewire, so I wanted it to play nicely with Alpine, which is a normal thing that should Just Work1 out of the box.
I started with TinyMCE, but I ran into a pretty serious bug that makes it unsuitable. I'm not really sure when to even start debugging that in its codebase ... so I moved on to something else I was familiar with: Quill.
AlpineJS Gotcha
I started out initializing it like I would anything else, and it appeared to work just fine:
<div
x-data="{ rte: null, getHTML: () => $refs.editor.querySelector('.ql-editor').innerHTML }"
x-init="
rte = new Quill($refs.editor, {/* config */});
$el.closest('form').addEventListener('formdata', (e) => e.formData.append($refs.editor.getAttribute('name'), getHTML()))
"
>
<div x-ref="editor" name="profile">
<!-- Editable user content here -->
</div>
</div>
This will initialize the editor. The second line in the x-init
finds the parent <form>
and does the plumbing to add the editor's contents to the form submission; this doesn't happen automatically because Quill's editor is a <div>
instead of a form input.
It'll work great right up until you want to customize the toolbar and call the format()
method, when you will start seeing blot is null
(or perhaps i is null
if it's minified) errors. I hit this when I was working on my own toolbar:
<div
x-data="{ rte: null, getHTML: () => $refs.editor.querySelector('.ql-editor').innerHTML }"
x-init="
rte = new Quill($refs.editor, { modules: { toolbar: $refs.toolbar } });
$el.closest('form').addEventListener('formdata', (e) => e.formData.append($refs.editor.getAttribute('name'), getHTML()))
"
>
<div x-ref="toolbar">
<!-- A lot of buttons, but here's the interesting one: -->
<div class="ql-formats">
<button class="ql-details" x-on:click="rte.format('bold', true, 'user')">
<i class="fa-regular fa-square-caret-down fa-fw d-block"></i>
</button>
</div>
</div>
<div x-ref="editor" name="profile">
<!-- ... -->
</div>
</div>
Clicking on that should make the text bold2, but would instead give me the blot is null
error. Here's an example on CodePen.
Solution
I got a hint from Quill issue 4132, where Daniel Pinto Salazar was having a similar problem with Vue properties:
It seems that using a reactive variable with the Quill editor can cause conflicts when events are generated within the editor. Using a non-reactive variable for the Quill instance resolved the issue for me.
In AlpineJS, all the variables under x-data
are being wrapped with a proxy object. That's how it monitors them for changes so all the reactive stuff works.
To solve it, I shuffled things around a little bit on my Alpine component <div>
:
<div
x-data="{
getHTML: () => $refs.editor.querySelector('.ql-editor').innerHTML,
init: () => {
rte = RichEditor($refs.editor, $refs.toolbar);
}
}"
x-init="$el.closest('form').addEventListener('formdata', (e) => e.formData.append($refs.editor.getAttribute('name'), getHTML()));">
The rte
variable is no longer an x-data
property, but remains available to the x-on:click
handlers in my toolbar. Here's a working CodePen for reference.