DEV Community

Cover image for Upgrade TinyMCE to v6 in Rails/Stimulus
djchadderton
djchadderton

Posted on • Edited on

Upgrade TinyMCE to v6 in Rails/Stimulus

A couple of years ago, I wrote a post on how to integrate the text editor TinyMCE into a Rails app using Stimulus.

While most of that post is still relevant, even after upgrading to Rails 7 and replacing Webpack with esbuild, the upgrade from v5 to v6 of TinyMCE did break a few things, so here's how to get it working again.

First of all, add the upgraded version to package.json by changing

"tinymce": "^5.8.0"
Enter fullscreen mode Exit fullscreen mode

to

"tinymce": "^6.3.0"
Enter fullscreen mode Exit fullscreen mode

and run the yarn command to update it.

In the Stimulus controller, as well as importing tinymce, you need to import the model, so the start of the controller file will read

import { Controller } from '@hotwired/stimulus'

// Import TinyMCE
import tinymce from 'tinymce/tinymce'
import 'tinymce/models/dom/model'
Enter fullscreen mode Exit fullscreen mode

Some plugins have been removed or incorporated into the main bundle, so delete any import statements for bbcode, colorpicker, contextmenu, fullpage, hr, imagetools, legacyoutput, noneditable, paste, print, spellchecker, tabfocus, textcolor, textpattern, toc.

Within your toolbar entries, rename styleselect to style, e.g.

toolbar: ['styles | bold italic underline strikethrough superscript | blockquote numlist bullist link | alignleft aligncenter alignright | table']
Enter fullscreen mode Exit fullscreen mode

Some other options have changed, so check the documentation on Migrating from TinyMCE 5 to TinyMCE 6 on the TinyMCE web site if anything isn't working as expected.

My current working Stimulus controller in full is:

import { Controller } from '@hotwired/stimulus'

// Import TinyMCE
import tinymce from 'tinymce/tinymce'
import 'tinymce/models/dom/model'

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

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

// 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/charmap'
import 'tinymce/plugins/code'
// import 'tinymce/plugins/codesample'
// import 'tinymce/plugins/directionality'
// import 'tinymce/plugins/emoticons'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/help'
// import 'tinymce/plugins/image'
// import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/link'
import 'tinymce/plugins/lists'
// import 'tinymce/plugins/media'
// import 'tinymce/plugins/nonbreaking'
// import 'tinymce/plugins/pagebreak'
import 'tinymce/plugins/preview'
// import 'tinymce/plugins/quickbars'
// import 'tinymce/plugins/save'
// import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/table'
// import 'tinymce/plugins/template'
// import 'tinymce/plugins/visualblocks'
// import 'tinymce/plugins/visualchars'
import 'tinymce/plugins/wordcount'

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

  initialize () {
    this.defaults = {
      autoresize_bottom_margin: 10,
      browser_spellcheck: true,
      content_css: false,
      content_style: `
        html {
          font-family: Roboto, sans-serif; line-height: 1.5;
        }
        h3, h4 {
          font-family: 'Montserrat', sans-serif;
          color: hsla(197, 55%, 26%, 1);
          line-height: 1;
          margin: .9rem 0;
        }
        `,
      invalid_elements: 'span',
      link_context_toolbar: true,
      link_default_target: '_blank',
      link_title: false,
      max_height: 700,
      menubar: false,
      mobile: {
        toolbar: [
          'styles | 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',
      relative_urls: false,
      skin: false,
      style_formats: [
          { title: 'Heading', format: 'h3' },
          { title: 'Sub Heading', format: 'h4' },
          { title: 'Sub Heading 2', format: 'h5' },
          { title: 'Sub Heading 3', format: 'h6' },
          { title: 'Paragraph', format: 'p'}
      ],
      toolbar: [
        'styles | bold italic underline strikethrough superscript | blockquote numlist bullist link | alignleft aligncenter alignright | table',
        'undo redo | fullscreen preview code help'
              ],
      valid_styles: { '*': 'text-align' }
    }
  }

  connect () {
    // Initialize TinyMCE
    if (!this.preview) {
      let config = Object.assign({ target: this.inputTarget }, this.defaults)
      this.tc = tinymce.init(config)
    }
  }

  disconnect () {
    if (!this.preview) tinymce.remove(this.tc.id)
  }

  get preview () {
    return document.documentElement.hasAttribute('data-turbo-preview')
  }
}
Enter fullscreen mode Exit fullscreen mode

Additional note for upgrading to v7

TinyMCE version 7 has added an extra requirement to specify either the open source GPLv2+ or a commercial licence in the config options object or it will warn in the console that it is running in evaluation mode. Add the following to this.defaults to continue using the open source version:

license_key: "gpl",
Enter fullscreen mode Exit fullscreen mode

Top comments (4)

Collapse
 
mxmb profile image
m.b.

I highly recommend using tinymce.remove(this.inputTarget) instead of just tinymce.remove(). Using it without an argument will destroy all the instances visible on the current page, not just the one binded to this specific controller instance.

Collapse
 
djchadderton profile image
djchadderton

Yes, you're correct. Thanks. I have implemented something like this since I wrote this article, but at first I found that if I went to another page and used the back button, the text box would either be uneditable or duplicated, suggesting it wasn't being removed properly. I ended up storing the initialised instance on connect and then removing it by passing its ID on disconnect. I'll edit the article to reflect this.

Collapse
 
philsmy profile image
Phil Smy

This isn't working for me. I have everything connected, and it gets to the tinymce.init(config) line and passes with no errors but nothing is on the page! It's quite strange.

I end up with this:

<div role="application" class="tox tox-tinymce" aria-disabled="false" style="visibility: hidden; height: 700px;">
.... whole bunch of other stuff
</div>
Enter fullscreen mode Exit fullscreen mode

We can see the box is hidden. But even manually changing that CSS doesn't give the right display.

Collapse
 
djchadderton profile image
djchadderton • Edited

That's what I get as well as the code TinyMCE replaces the textarea tag with, so it looks like the javascript is running. I'm not sure what to suggest, other than going through all of the different options and making sure they're correctly set, especially any that have changed their names (like styleselect to style), the ones I've mentioned above and others in the link to the official documentation.

In fact, I may start by disabling all but the most basic options to see if that works and then re-enabling them gradually to see where it breaks.