Getting the Trix Rich Text Editor up and running in Rails is pretty straightforward but getting the heading button to generate H2 tags instead of the default H1 seemed to be way more complicated than it should have been. All the guides I read promised it was a simple case of editing the Trix.config before the trix-before-initialize event is fired:

document.addEventListener("trix-before-initialize", () => {
	Trix.config.blockAttributes.heading1 = {
		tagName: "h2",
		terminal: true,
		breakOnReturn: true,
		group: false
	}
})

I added the above code into a Stimulus controller and at first everything seemed to work but the more I played with it the more it misbehaved. Sometimes the heading button would work as expected, other times it would convert all my headings into <strong> tags, and sometimes it would seem to show h2 tags in the editor but viewing the content using the ‘show’ action would display h1 tags. Intermitent problems are always very frustrating, in this case they were caused by using Turbo.

Turbo is preventing the trix-before-initialize event from firing

My app used Turbo frames for displaying the Trix editor and that meant that in certain circumstances the trix-before-initialize event would be fired too late leaving Trix with the default config instead of my new one. This wasn’t easy to diagnose as when I logged the Trix config in the console it appeared to have acquired the new config but in fact it only acquired the new config after it built the toolbar and the editor window.

I found an issue on Trix’s Github titled Extened toolbar not restoring content correctly and a pull request titled Don’t start Trix automatically on load, both of which tipped me off that Trix is not broken it’s just the order in which things are executed preventing the headings to work as expected.

How to get Trix to use H2 tags instead of H1

To change Trix’s heading button from H1 to H2 and to ensure that Trix always uses this new config I chose the down-and-dirty method of adding a script tag into my view. It ain’t pretty but it works every time, and it works with my next challenge of adding a new h3 button to the Trix toolbar (more about that coming up).

<%= form_with(model: [:admin, post]) do |form| %>
	<%= form.rich_text_area :content %>
	<%= form.submit 'Save' %>
<% end %>

<script>
	document.addEventListener("trix-before-initialize", () => {
		// Make the 'Heading' button generate h2 tags
		// instead of the default h1 tags.
		Trix.config.blockAttributes.heading1 = {
			tagName: "h2",
			terminal: true,
			breakOnReturn: true,
			group: false
		}
	})
</script>

This is the standard block of code you’ll find on all the other guides on the internet, the only difference with mine is that it’s been added to the _form.html.erb view to ensure it works regardless of whatever magic Turbo might be performing.

How to add H3 button to Trix

Adding buttons to the Trix toolbar is a 2 step process: First we modify the Trix config similar to the previous step, then we use Javascript to stick a new button into the desired position on the toolbar. Sounds easy but before I figured out whole trix-before-initalize-not-always-firing thing described above I was getting all sorts if intermittent issues such as:

There was another cause of these bugs too, this one was related to the difference between the trix-before-initialize and trix-initialize events: Changing the Trix config must use the trix-before-initialize event where as adding buttons to the Trix toolbar must use the trix-initialize event. Some of the guides I found while troubleshooting don’t make this absolutely clear, or perhaps it wasn’t necessary in their use case? Or most likely is that I wasn’t paying enough attention to notice the subtle difference.

Here is how to solve both of my Trix requirements; a heading button that renders h2 tags instead of h1, and an extra button which lets the user add h3 tags. The whole thing is just stuck onto the _form.html.erb which may not be ideal but it’s where it stays until I decide to figure out the whole script execution thing.

<%= form_with(model: [:admin, post]) do |form| %>
	<%= form.rich_text_area :content %>
	<%= form.submit 'Save' %>
<% end %>

<script>
	document.addEventListener("trix-before-initialize", () => {

		// Make the 'Heading' button generate h2 tags
		// instead of the default h1 tags.
		Trix.config.blockAttributes.heading1 = {
			tagName: "h2",
			terminal: true,
			breakOnReturn: true,
			group: false
		}

		// Create a new button for adding h3 tags.
		Trix.config.blockAttributes.heading3 = {
			tagName: "h3",
			terminal: true,
			breakOnReturn: true,
			group: false
		}
	})

	document.addEventListener("trix-initialize", () => {

		// See the notes section of this guide for more info about the class and
		// style attributes added to this button.
		const h3ButtonHtml = `
			<button
				type="button"
				class="trix-button trix-button--icon"
				style="text-indent: 0;"
				data-trix-attribute="heading3"
				title="Heading 3"
				tabindex="-1"
				data-trix-active=""
			>H3</button>
		`

		// Add a new button to the toolbar (but only do so if a h3 button
		// doesn't already exists otherwise Turbo frames will duplicate the
		// button each time it reloads the frame)
		if (!document.querySelector("[data-trix-attribute=heading3]")) {
			const h1Button = document.querySelector("[data-trix-attribute=heading1]")
			h1Button.insertAdjacentHTML("afterend", h3ButtonHtml)
		}
	})
</script>

Notes for modifying the Trix toolbar

Fixing the duplicate Trix buttons issue

The only issue with my solution for the extra h3 button was that reloading the turbo frame would cause an extra h3 button to be created and added to the Trix toolbar, I got around this with a simple ‘if’ statement.

Note that this code could have worked inside a Stimulus controller but as the rest of my Trix related code was already in the form view I just stuck it there for consistency.

if (!document.querySelector("[data-trix-attribute=heading3]")) {
	const h1Button = document.querySelector("[data-trix-attribute=heading1]")
	h1Button.insertAdjacentHTML("afterend", h3ButtonHtml)
}

Styling the Trix buttons

The default Trix buttons use Google’s Material Design Icons which are available in the repo as SVG files then loaded in as background images by toolbar.scss. Instead of using a SVG for my new h3 button I chose to use text which looks fine provided you add the text-indent: 0 style which is required to make the text visible on the button.

class="trix-button trix-button--icon"
style="text-indent: 0;"

Try using a CDN when troubleshooting Trix with Rails

If your Trix editor is doing weird things or not accepting a new configuration then consider temporarily removing it from Rails and loading it up via a CDN instead. This will allow you to avoid any issues caused by either Rails or Stimulus loading scripts in an order which causes Trix to bug out.

Konnor Rogers guide to using Trix

Here’s a 4 part series on modifying Trix written by someone who clearly knows way more about it than I do. In part 2 of the guide he describes using Trix with lazy-loaded Turbo frames as “an ever-increasing ball of complexity” which was reassuring to know after I’d spent several hours convinced that my challenges were irrefutable proof that I was in fact a total retard who had somehow fooled everyone into believing I could code.

Konnor’s guides will probably reveal “the correct way” of modifying Trix instead of the “the half-assed pleb way” which I’ve used in this guide, but after spending hours battling to get such a trivial feature added I’m leaving “the correct way” for another day …or another project? …maybe another lifetime?