This Stimulus controller will let you easily integrate Quill.js into your Rails project, including adding some missing table functionality.
I’ve used lots of different WYSIWYG editors in the past, most are clunky or produce messy HTML, but Quill.js is a pretty light weight option that actually works fairly well. The following will show how to use Stimulus to create a Quill editor as well as add some missing table functionality.
Add Quill Module
Note, the table module is only available in the upcoming 2.0.0 release, so you’ll need to add the latest dev version via yarn.
yarn add quill@2.0.0-dev.4
Or install it via npm.
npm install quill@2.0.0-dev.4
Create Quill Editor Partial
The partial has two important parts, the hidden field which will get updated via Stimulus so it can be submitted with the Rails form, and div container which will host the Quill editor that is initialized by Stimulus. It also is connecting Stimulus targets and actions.
<%= form.hidden_field :html, data: { quill: { target: "input" } } %>
<div style="min-height:500px;" data-quill-target="editor"></div>
Add Stimulus Controller
For the Stimulus controller, we have two targets editor and input so we can access both the hidden field as well as the div container.
In the connect method, we instantiate Quill passing in the theme and modules we’d like to use. Since we will be using the table feature, we enable that module. We also include the toolkit module passing in options for the buttons we want to show.
import ApplicationController from './application_controller';
import Quill from 'quill';
export default class extends ApplicationController {
static targets = ["editor", "input"]
connect() {
this.quill = new Quill(this.editorTarget, {
theme: 'snow',
modules: {
table: true,
toolbar: [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
['bold', 'italic', 'underline'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'indent': '-1' }, { 'indent': '+1' }],
[{ 'align': [] }],
['blockquote', 'link', 'image'],
['clean']
]
}
});
this.quill.container.firstChild.innerHTML = this.inputTarget.value;
this.quill.on('text-change', (delta, oldDelta, source) => {
this.inputTarget.value = this.quill.container.firstChild.innerHTML;
});
}
disconnect() {
if (this.quill) {
this.quill = null;
}
}
}
After instantiating Quill, we then set it’s content with the current value from the hidden field, this is important if you are editing an existing record that already has some content. Then we listen to the text-change event, setting the content from Quill back to the hidden field, so when the form is submitted the HTML content will get included.
And we should now have a working WYSIWYG editor!
Set Input Area Focusable
One issue I noticed was that clicking inside the input area does not always focus the control for typing. For example clicking the top area takes focus but clicking bottom area does not. So let's add Stimulus click action to set the focus.
<div style="min-height:500px;" data-quill-target="editor" data-action="click->quill#focus"></div>
focus() {
this.quill.focus();
}
Add Table Functionality
The upcoming 2.0.0 release of Quill will have a table module, although currently there’s only a button for adding a table and no buttons for adding new rows or columns. So let’s add that functionality now.
In your toolbar options, add the following line which will show Quill’s table button as well as six of our own custom buttons.
['table', 'column-left', 'column-right', 'row-above', 'row-below', 'row-remove', 'column-remove']
Now inside your connect method, we get reference to the table module so we can call it later.
this.table = this.quill.getModule('table');
Next we connect the column-left button, including setting it’s icon to a SVG.
document.querySelectorAll('.ql-column-left').forEach(button => {
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-left-square" viewBox="0 0 16 16"><path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/><path d="M10.205 12.456A.5.5 0 0 0 10.5 12V4a.5.5 0 0 0-.832-.374l-4.5 4a.5.5 0 0 0 0 .748l4.5 4a.5.5 0 0 0 .537.082z"/></svg>'
button.title = "Add column left";
button.addEventListener('click', () => {
this.table.insertColumnLeft();
});
});
Now the same for the column-right button.
document.querySelectorAll('.ql-column-right').forEach(button => {
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-right-square" viewBox="0 0 16 16"><path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/><path d="M5.795 12.456A.5.5 0 0 1 5.5 12V4a.5.5 0 0 1 .832-.374l4.5 4a.5.5 0 0 1 0 .748l-4.5 4a.5.5 0 0 1-.537.082z"/></svg>';
button.title = "Add column right";
button.addEventListener('click', () => {
this.table.insertColumnRight();
});
});
Now the row-above button.
document.querySelectorAll('.ql-row-above').forEach(button => {
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-up-square" viewBox="0 0 16 16"><path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/><path d="M3.544 10.705A.5.5 0 0 0 4 11h8a.5.5 0 0 0 .374-.832l-4-4.5a.5.5 0 0 0-.748 0l-4 4.5a.5.5 0 0 0-.082.537z"/></svg>';
button.title = "Add row above";
button.addEventListener('click', () => {
this.table.insertRowAbove();
});
});
Next the row-below button.
document.querySelectorAll('.ql-row-below').forEach(button => {
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-down-square" viewBox="0 0 16 16"><path d="M3.626 6.832A.5.5 0 0 1 4 6h8a.5.5 0 0 1 .374.832l-4 4.5a.5.5 0 0 1-.748 0l-4-4.5z"/><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2z"/></svg>'
button.title = "Add row below";
button.addEventListener('click', () => {
this.table.insertRowBelow();
});
});
Now the row-remove button.
document.querySelectorAll('.ql-row-remove').forEach(button => {
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dash-square" viewBox="0 0 16 16"><path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/><path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/></svg>';
button.title = "Remove row";
button.addEventListener('click', () => {
this.table.deleteRow();
});
});
Finally the column-remove button.
document.querySelectorAll('.ql-column-remove').forEach(button => {
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-square" viewBox="0 0 16 16"><path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>';
button.title = "Remove column";
button.addEventListener('click', () => {
this.table.deleteRow();
});
});
That’s it, you now have ability to add a table, add columns, add rows, remove columns and remove rows!
The icons don't perfectly match but you can easily change those icons to whatever SVGs you'd prefer instead.
Note, the console will show some warnings for your custom buttons, like quill:toolbar ignoring attaching to nonexistent format column-left. Hopefully when Quill.js 2.0.0 is released, it will include these toolbar buttons as options, but in the meantime you can still use this functionality. Enjoy!
You can find the full solution at https://gist.github.com/dalezak/834198114f414a79de355e0c2cb664d3.
Top comments (1)
Thanks, man! This is very helpful. You have saved me. For some reason I was not able to find a decent example in the documentation, and the table for the quill 1 is very buggy.