Using Tables In Quill.js With Rails and Stimulus
Tags: rails, stimulus, quilljs
Published: 2021-03-25 15:45:13

This [Stimulus](https://stimulus.hotwire.dev) controller will let you easily integrate [Quill.js](https://quilljs.com) into your [Rails](https://rubyonrails.org) project, including adding some missing table functionality.

![quill](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0o7qc8vea8xby28us0xd.png) 

I’ve used lots of different WYSIWYG editors in the past, most are clunky or produce messy HTML, but [Quill.js](https://quilljs.com) is a pretty light weight option that actually works fairly well. The following will show how to use [Stimulus](https://stimulus.hotwire.dev) to create a [Quill](https://quilljs.com) editor as well as add some missing table functionality.

---

### Add Quill Module
Note, the [table module](https://github.com/quilljs/quill/blob/e9f628b5562e78126291bb25f63d0dfc3f6c6f86/modules/table.js) 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](https://stimulus.hotwire.dev/reference/targets) and [actions](https://stimulus.hotwire.dev/reference/actions).
```erb
<%= 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](https://quilljs.com/docs/themes/) and [modules](https://quilljs.com/docs/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](https://quilljs.com/docs/modules/toolbar/) we want to show.

```ruby
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_!

![editor_1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k7sqtm5ntz1hmaoz1sdk.png) 

---

### 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.

```erb
<div style="min-height:500px;" data-quill-target="editor" data-action="click->quill#focus"></div>
```

```javascript
focus() {
 this.quill.focus();
}
```

---

### Add Table Functionality
The upcoming 2.0.0 release of Quill will have a [table module](https://github.com/quilljs/quill/blob/e9f628b5562e78126291bb25f63d0dfc3f6c6f86/modules/table.js), 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.

```javascript
['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.

```javascript
this.table = this.quill.getModule('table');
```

Next we connect the _column-left_ button, including setting it’s icon to a SVG.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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!

![editor_2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mh07v2w6ktb2og59qk5h.png) 

---

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](https://gist.github.com/dalezak/834198114f414a79de355e0c2cb664d3).