Making TinyMCE work with Rails, Turbolinks and Stimulus
Tags: ruby, rails, javascript, stimulus
Published: 2021-05-14 15:55:41

[TinyMCE](https://www.tiny.cloud/tinymce/) is a great text editor as a drop-in replacement for `textarea` fields in forms, but it doesn't play nicely with Turbolinks, or anything else that doesn't do full page refresh. I've seen a lot of tips for forcing TinyMCE to unload itself through JavaScript before Turbolinks inserts the new content, but I could never get any of them to work. Even the tinymce-rails gem, which is supposed to have the fix built-in, never worked for me.

In the end, I resorted to using the tinymce-rails gem but inserting `data-turbolinks-"false"` into every link to a page with a form on it to force Turbolinks to do a full page refresh. Not an ideal solution.

Finally, I decided to try to crack how to use TinyMCE properly through Webpack on Rails 6 without any reconfiguring of Webpack itself. Different bits of the solution came from different places, so I've brought together here the method that worked for me.

First of all, in a Rails project with Turbolinks included and StimulusJS installed, install the TinyMCE package through Yarn.

```javascript
yarn add tinymce
```

In `app/javascript/controllers`, create the file `tinymce_controller.js`. Start with the usual blank Stimulus controller:

```javascript
import { Controller } from 'stimulus'

export default class extends Controller {
}
```

You'll need to import TinyMCE itself, plus icons, a theme and a skin from the node package. This will load the included defaults:

```javascript
// Import TinyMCE
import tinymce from 'tinymce/tinymce'

// Import icons
import 'tinymce/icons/default/icons'

// Import theme
import 'tinymce/themes/silver/theme';

// Import skin
import 'tinymce/skins/ui/oxide/skin.min.css';
```

You will also need to import each plugin that you intend to use, one at a time, for instance:

```javascript
import 'tinymce/plugins/autoresize';
import 'tinymce/plugins/code';
import 'tinymce/plugins/fullscreen';
```

Inside the export, set a target name for the textarea tag:

```javascript
static targets = ['input']
```

Set all of your default settings in an initializer method. Make sure you set `content_css: false` and `skin: false` as you have already imported both of these so you don't want TinyMCE to look for them in a separate file and give an error when it can't find them. For all other settings, see the [TinyMCE main documentation](https://www.tiny.cloud/docs/). Here is my method.

```javascript
initialize () {
  this.defaults = {
    content_css: false,
    skin: false,
    toolbar: [
      'styleselect | bold italic underline strikethrough superscript | blockquote numlist bullist link | alignleft aligncenter alignright | table',
      'undo redo | fullscreen preview code help'
            ],
    mobile: {
      toolbar: [
        'styleselect | bold italic underline strikethrough superscript',
        'blockquote numlist bullist link | alignleft aligncenter alignright | table',
        'undo redo | fullscreen preview code help'
      ]
    },
    plugins: 'link lists fullscreen help preview table code autoresize wordcount',
    menubar: false,
    style_formats: [
      { title: 'Heading 1', format: 'h1' },
      { title: 'Heading 2', format: 'h2' },
      { title: 'Heading 3', format: 'h3' },
      { title: 'Heading 4', format: 'h4' },
      { title: 'Heading 5', format: 'h5' },
      { title: 'Heading 6', format: 'h6' },
      { title: 'Paragraph', format: 'p'}
    ],
    max_height: 700,
    default_link_target: '_blank',
    link_title: false,
    autoresize_bottom_margin: 10,
    link_context_toolbar: true,
    relative_urls: false,
    browser_spellcheck: true,
    element_format: 'html',
    invalid_elements: ['span'],
    content_style: 'html { font-family: Roboto, sans-serif; line-height: 1.5; }'
  }
}
```

The `connect` method initiates the app and applies the settings.

```javascript
connect () {
  let config = Object.assign({ target: this.inputTarget }, this.defaults)
  tinymce.init(config)
}
```

To make sure the editor loads properly on a page change or a failed submit rather than just showing a `textarea`, you must include a `disconnect` method to destroy the app instance.

```javascript
disconnect () {
  tinymce.remove()
}
```

In your header (for instance in your `application.html.erb` file), make sure you include pack tags for both the javascript and the css:

```javascript
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
```

(If you are using Turbo, the successor to Turbolinks, change `data-turbolinks-track` to `data-turbo-track`.)

On your form, you need to include the tinymce controller in the `div` surrounding your `textarea` and add the target name to the `textarea` itself, e.g.

```javascript
<div class="field" data-controller="tinymce">
  <=% f.label :body %>
  <%= f.text_area :body, data: { tinymce_target: 'input' } %>
</div>
```

And that should work. You can even include more than one text box on the same page and they should function independently without interfering with one another.

The full Stimulus controller code that I used with all of the plugins in the current standard package (v5.8.0) to be uncommented if required is below.

```javascript
import { Controller } from 'stimulus'

// Import TinyMCE
import tinymce from 'tinymce/tinymce'

// Import icons
import 'tinymce/icons/default/icons'

// Import theme
import 'tinymce/themes/silver/theme';

// Import skin
import 'tinymce/skins/ui/oxide/skin.min.css';

// Import plugins

// import 'tinymce/plugins/advlist';
// import 'tinymce/plugins/anchor';
// import 'tinymce/plugins/autolink';
import 'tinymce/plugins/autoresize';
// import 'tinymce/plugins/autosave';
// import 'tinymce/plugins/bbcode';
// import 'tinymce/plugins/charmap';
import 'tinymce/plugins/code';
// import 'tinymce/plugins/codesample';
// import 'tinymce/plugins/colorpicker';
// import 'tinymce/plugins/contextmenu';
// import 'tinymce/plugins/directionality';
// import 'tinymce/plugins/emoticons';
// import 'tinymce/plugins/fullpage';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/help';
// import 'tinymce/plugins/hr';
// import 'tinymce/plugins/image';
// import 'tinymce/plugins/imagetools';
// import 'tinymce/plugins/insertdatetime';
// import 'tinymce/plugins/legacyoutput';
import 'tinymce/plugins/link';
import 'tinymce/plugins/lists';
// import 'tinymce/plugins/media';
// import 'tinymce/plugins/nonbreaking';
// import 'tinymce/plugins/noneditable';
// import 'tinymce/plugins/pagebreak';
// import 'tinymce/plugins/paste';
import 'tinymce/plugins/preview';
// import 'tinymce/plugins/print';
// import 'tinymce/plugins/quickbars';
// import 'tinymce/plugins/save';
// import 'tinymce/plugins/searchreplace';
// import 'tinymce/plugins/spellchecker';
// import 'tinymce/plugins/tabfocus';
import 'tinymce/plugins/table';
// import 'tinymce/plugins/template';
// import 'tinymce/plugins/textcolor';
// import 'tinymce/plugins/textpattern';
// import 'tinymce/plugins/toc';
// import 'tinymce/plugins/visualblocks';
// import 'tinymce/plugins/visualchars';
import 'tinymce/plugins/wordcount';

export default class extends Controller {
  static targets = ['input']

  initialize () {
    this.defaults = {
      content_css: false,
      skin: false,
      toolbar: [
        'styleselect | bold italic underline strikethrough superscript | blockquote numlist bullist link | alignleft aligncenter alignright | table',
        'undo redo | fullscreen preview code help'
              ],
      mobile: {
        toolbar: [
          'styleselect | bold italic underline strikethrough superscript',
          'blockquote numlist bullist link | alignleft aligncenter alignright | table',
          'undo redo | fullscreen preview code help'
        ]
      },
      plugins: 'link lists fullscreen help preview table code autoresize wordcount',
      menubar: false,
      style_formats: [
        { title: 'Heading 1', format: 'h1' },
        { title: 'Heading 2', format: 'h2' },
        { title: 'Heading 3', format: 'h3' },
        { title: 'Heading 4', format: 'h4' },
        { title: 'Heading 5', format: 'h5' },
        { title: 'Heading 6', format: 'h6' },
        { title: 'Paragraph', format: 'p'}
      ],
      max_height: 700,
      default_link_target: '_blank',
      link_title: false,
      autoresize_bottom_margin: 10,
      link_context_toolbar: true,
      relative_urls: false,
      browser_spellcheck: true,
      element_format: 'html',
      invalid_elements: ['span'],
      content_style: 'html { font-family: Roboto, sans-serif; line-height: 1.5; }'
    }
  }

  connect () {
    let config = Object.assign({ target: this.inputTarget }, this.defaults)
    tinymce.init(config)
  }

  disconnect () {
    tinymce.remove()
  }
}
```

NB This article was written to work with TinyMCE 5. The upgrade to v6 broke a few things; see [my follow-up article](https://dev.to/djchadderton/upgrade-tinymce-to-v6-in-railsstimulus-1c1e) for how I managed to fix it.