Let’s create a Trix Editor plugin using Stimulus.js

I’ve been working on an app that uses Trix to edit a very complicated book text. Someone asked if I could add the ability to choose different sizes of text while they are editing the book. After digging around online, I came across this example from Javan, one of Trix’s creators: https://codepen.io/javan/pen/EPqzZo. This was exactly what I wanted, only with different actions.

After looking into Trix’s source code to figure out all the attributes the buttons needed, I added some H2 and H3 buttons to the title bar with this code:

<script>
  (function() {
	Trix.config.blockAttributes.heading2 = {
	  tagName: "h2",
	  terminal: true,
	  breakOnReturn: true,
	  group: false
	}
	Trix.config.blockAttributes.heading3 = {
	  tagName: "h3",
	  terminal: true,
	  breakOnReturn: true,
	  group: false
	}

	var h2ButtonHTML = '<button type="button" data-trix-attribute="heading2" title="Subheading">H2</button>'
	var h3ButtonHTML = '<button type="button" data-trix-attribute="heading3" title="Subheading">H3</button>'

	
  	var groupElement = Trix.config.toolbar.content.querySelector(".block_tools")

  	groupElement.insertAdjacentHTML("beforeend", h2ButtonHTML)
	groupElement.insertAdjacentHTML("beforeend", h3ButtonHTML)
  }).call(this);
</script>

<trix-editor input="book_text"></trix-editor>
<input id="book_text" value="" type="hidden" name="content">

The code worked really well, but I wasn’t too happy with how it looked, since this was all over the HTML of the page.

I mulled it over for a while, and realized that this could all go inside the connect() function of a Stimulus controller. I could even name the controller something descriptive like h2_plugin_controller.js, and my html would look clean and make sense, and I could wrap the code into different controllers, adding them to an editor when they made sense.

The above code was then refactored into two different controllers.

Here is trix_h2_plugin_controller.js:

import { Controller } from "stimulus"

export default class extends Controller {

  connect() {
	Trix.config.blockAttributes.heading2 = {
	  tagName: "h2",
	  terminal: true,
	  breakOnReturn: true,
	  group: false
	}
	
	var h2ButtonHTML = '<button type="button" data-trix-attribute="heading2" title="Subheading">H2</button>'
	var groupElement = Trix.config.toolbar.content.querySelector(".block_tools")
	groupElement.insertAdjacentHTML("beforeend", h2ButtonHTML)
  }
}

And here is trix_h3_plugin_controller.js:

import { Controller } from "stimulus"

export default class extends Controller {

  connect() {
	Trix.config.blockAttributes.heading3 = {
	  tagName: "h3",
	  terminal: true,
	  breakOnReturn: true,
	  group: false
	}
	
	var h3ButtonHTML = '<button type="button" data-trix-attribute="heading3" title="Subheading">H3</button>'
	var groupElement = Trix.config.toolbar.content.querySelector(".block_tools")
	groupElement.insertAdjacentHTML("beforeend", h3ButtonHTML)
  }
}

My html becomes much cleaner with just some Stimulus annotations:

<trix-editor input="book_text" data-controller="trix-h2-plugin trix-h3-plugin"></trix-editor>
<input id="book_text" value="" type="hidden" name="content">

I hope this helps give you some ideas for the flexibility of Stimulus’ controller model. You are able to drop little bits of functionality wherever you need them, even inside other Javascript libraries.

Feel free to leave a comment or question below.

Want To Learn More?

Try out some more of my Stimulus.js Tutorials.

5 thoughts on “Let’s create a Trix Editor plugin using Stimulus.js

  1. Well, great idea. The h2 and h3 tags are saved to the database but as soon as a try to edit a saved record the tags are not shown in the editor.

    1. Do you have the Trix blockattributes?

      You also can try adding them to application.js:

      import “trix”;
      import “@rails/actiontext”;

      Trix.config.blockAttributes.heading2 = {
      tagName: “h2”,
      terminal: true,
      breakOnReturn: true,
      group: false,
      };

      Trix.config.lang.heading2 = “H2”;

      Trix.config.blockAttributes.heading3 = {
      tagName: “h3”,
      terminal: true,
      breakOnReturn: true,
      group: false,
      };

      Trix.config.lang.heading3 = “H3”;

      1. I’ve been playing with this, and it seems that if you navigate to the page via Turbo, the h2 and the h3 seem to render fine. If you reload the page, then the Trix object hasn’t yet been configured, so it gets rid of the h2, since those haven’t been configured yet.

        I have one work around for my demo, I’m curious how it would work with other text:

        Add the value as a data attribute, and then in the connect function, reset the text in the editor.

        HTML:
        <%= form.rich_text_area :details, data: { controller: 'rich-text-plugin-h2 rich-text-plugin-h3', value: todo.details.body.to_html } %>

        JavaScript:
        this.element.value = this.element.dataset[“value”];

    1. You’re welcome! I have it working correctly on an app using Webpacker, and it’s as you described in an app using Propshaft. I’m not sure where the difference in packaging is.

Leave a Reply