DEV Community

Dale Zak
Dale Zak

Posted on • Updated on • Originally published at dalezak.Medium

Using Tables In Quill.js With Rails and Stimulus

This Stimulus controller will let you easily integrate Quill.js into your Rails project, including adding some missing table functionality.

quill

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
Enter fullscreen mode Exit fullscreen mode

Or install it via npm.

npm install quill@2.0.0-dev.4
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
 }
}
Enter fullscreen mode Exit fullscreen mode

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!

editor_1


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>
Enter fullscreen mode Exit fullscreen mode
focus() {
 this.quill.focus();
}
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

Now inside your connect method, we get reference to the table module so we can call it later.

this.table = this.quill.getModule('table');
Enter fullscreen mode Exit fullscreen mode

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();
 });
});
Enter fullscreen mode Exit fullscreen mode

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();
 });
});
Enter fullscreen mode Exit fullscreen mode

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();
 });
});
Enter fullscreen mode Exit fullscreen mode

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();
 });
});
Enter fullscreen mode Exit fullscreen mode

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();
 });
});
Enter fullscreen mode Exit fullscreen mode

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();
 });
});
Enter fullscreen mode Exit fullscreen mode

That’s it, you now have ability to add a table, add columns, add rows, remove columns and remove rows!

editor_2


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)

Collapse
 
saulodias profile image
Saulo Dias

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.