Modifying the Trix toolbar headings buttons when using Rails Turbo frames
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.
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.
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.
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:
<strong>
tagsThere 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>
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)
}
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;"
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.
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?