With version 6, Rails introduced a system to make it easy to enhance text areas with WYSIWYG (What You See Is What You Get) editors. Often, developers need to allow users—who may not be HTML-savvy—to edit HTML content. Rails developers have previously needed to choose from a variety of JavaScript-based WYSIWYG editors, integrate the assets into the Asset Pipeline, and connect them with the desired text area inputs. While Rails developers still have the freedom to do this if we desire, Action Text gives us first-class option.
In this chapter, we’ll cover the steps necessary to allow the users in our blog application to easily use HTML when editing their articles’ body fields.
Installation
To add support for using Action Text, we’ll need to do a few things. First, Action Text stores its data in a separate table (similar to Active Storage, as we saw in the previous chapter). Thankfully, Rails gives us a simple way to generate the needed database migrations. Next, we’ll need to include Action Text’s JavaScript and CSS in our application.
Added Action Text’s CSS to our app
Added Action Text’s JavaScript dependencies to our package.json and yarn.lock files
Installed Action Text’s JavaScript dependencies into our node_modules directory
Added Action Text’s JavaScript to our app’s JavaScript pack
Created the database migration we need to store Action Text data
Next we’ll describe each of the changes that just happened in a little more detail. We could just skip ahead to enhancing a blog, but we can learn a little bit by paying closer attention to these details.
Action Text CSS
The action_text:install command we ran magically added Action Text’s CSS to our application, so that the Trix editor (Action Text’s WYSIWYG editor of choice) has the styles it needs to look good.
Excerpt from app/assets/stylesheets/application.csshttps://gist.github.com/nicedawg/92ed0bf488b5b4aee1eef85982f8383a
The comments are helpful here, so read them closely. Essentially, they’re saying you could add a special comment line like /*= require ‘shiny’ */ and Rails will look for a CSS file named “shiny.scss” or “shiny.css” in a few of your application’s directories. If it finds one, it will include it in your application.css file. If it doesn’t find one in your app’s directories, it will then start looking in your included gems for a style sheet with the given name.
This is good to know, but it doesn’t answer our question—how did Action Text’s CSS actually get added to our application.css? The answer is in the bolded line—the “= require_tree .” directive in our comment says “look for any style sheets in this directory and any subdirectories and include them.” Since the action_text:install command added app/assets/stylesheets/actiontext.scss, the require_tree directive included that style sheet automatically.
This is a great example of how Rails again uses convention over configuration to simplify the process of adding a style sheet. However, there may be times when require_tree is undesirable. Perhaps you need to control the order of style sheet inclusion, or maybe you need to exclude certain style sheets from being included. In those cases, you may need to use the require directive to explicitly list the style sheets you wish to include. We won’t need to do that for our application, but it’s good to know this exists. For more information on these directives (like require_tree and require), see https://guides.rubyonrails.org/asset_pipeline.html#manifest-files-and-directives.
app/assets/stylesheets/actiontext.scsshttps://gist.github.com/nicedawg/aa815191362c98adb5e445c4e803ea82
This style sheet is included in our main style sheet. Notice the bolded text. This style sheet includes another style sheet—trix/dist/trix. Where does that come from? We didn’t notice that getting added to our application. The require directive also loads from our node_modules directory. If you look in node_modules/trix/dist, you’ll see a trix.css file. Apparently, Trix’s default CSS needed just a bit of tweaking to work with Action Text, so our actiontext.scss adds some overrides.
Action Text JavaScript
We just learned how Action Text integrated its CSS into our app; now, we’ll take a look at how Action Text integrated its JavaScript into our app.
An entry was added to package.json which listed trix as a JavaScript dependency, requiring the version number to begin with 1.
An entry was added to package.json which listed @rails/actiontext as a JavaScript dependency, requiring the version number to begin with 6.
The latest versions of those JavaScript libraries (which satisfied the version requirements) were downloaded into our app’s node_modules directory.
The exact versions of these two JavaScript libraries (and their dependencies) were added to yarn.lock, to ensure that future installations of these libraries for our app will receive the exact same versions.
app/javascripts/packs/application.jshttps://gist.github.com/nicedawg/b647350cce50f921b2e8ab666474b841
Similar to how Action Text’s CSS was included in our application.css, the bolded require lines include Action Text’s JavaScript in our main application.js pack. We can use require in our JavaScript files to include other JavaScript files from our own application (e.g., app/javascript/shiny.js) or to include JavaScript files from our app’s dependencies found in our node_modules directory. (The latter is where our trix and @rails/actiontext JavaScript dependencies are found.)
Action Text Database Storage
Action Text Table Added to db/schema.rbhttps://gist.github.com/nicedawg/b4d79afe726e026afae59821a3b70385
Similar to the active_storage_attachments table from the previous chapter’s work, the action_text_rich_texts table is a polymorphic table, meaning it can belong to many different types of Active Record models by using the record_type column to store the class name of the model a particular action_text_rich_texts row belongs to, along with the record_id column to identify which particular record (of type record_type) it belongs to.
Sample Data in the action_text_rich_texts Database Table
id | name | body | record_type | record_id |
---|---|---|---|---|
1 | body | <p>Hey!</p> | Article | 1 |
2 | body | <p>Yo</p> | FAQ | 1 |
3 | excerpt | <p>Hi</p> | Article | 1 |
4 | body | <p>:-)</p> | Article | 2 |
Article 1 has a body of “<p>Hey!</p>” and an excerpt of “<p>Hi</p>”.
FAQ 1 has a body of “<p>Yo</p>”.
Article 2 has a body of “<p>:-)</p>”, but no excerpt.
As you can see, this structure is very flexible. It can hold the content for any attribute of any model. This usually is much more convenient than needing to define separate database storage each model’s rich text needs.
Using Action Text in Our Blog
Now that we’ve investigated how Action Text adds its CSS, JavaScript, and database storage needs to our app, we can enhance our blog with greater understanding of what’s happening behind the scenes. In the following steps, we will allow our users to create articles with HTML bodies.
Updating the Article Model
First, we need to make our Article model aware that we want to use Action Text for its body attribute.
Adding Action Text to Article#bodyhttps://gist.github.com/nicedawg/98424877354b24411de2dbe5d2a1fa79
By adding has_rich_text :body, a few things happened behind the scenes. Now, Article#body is a has_one relation which returns the relevant ActionText::RichText object from the action_text_rich_texts table, rather than returning the value of the body column from the articles table. Also, assigning to the body (e.g., article.body = “<p>Hey!</p>”) now assigns the given value to the body attribute of the ActionText::RichText related object, rather than the article’s old body attribute. Lastly, adding has_rich_text :body added a couple of scopes to our class to make it easier to include the related ActionText::RichText objects, helping us avoid N+1 queries. We’ll demonstrate this later in the chapter.
Migrating Our Data
You may have noticed in the previous section that one side effect of using has_rich_content :body is that the value is retrieved from (and stored in) a different table. If we were starting from scratch, no problem. However, we have some data in our articles table’s body column which is now being ignored! To preserve our data (and for illustration), we’ll migrate our body data from the articles table to the action_text_rich_texts table. Then we’ll add a migration to remove the body column from the articles table; since we no longer need it, we should remove it to prevent possible confusion down the road.
Migrating Article Body Data to ActionText::RichTexthttps://gist.github.com/nicedawg/5654a9de5ac781d71462912af754d659
Let’s talk about this migration. First, notice we defined separate up and down methods in this migration; we need to execute some custom SQL, so we need to declare what should happen in each direction.
When migrating up, we added some SQL to create action_text_rich_texts records using our articles data. We used the INSERT INTO .. SELECT... syntax which most SQL databases understand. It may look complicated, but it’s essentially saying “for each record in the articles table, create a record in the action_text_rich_texts by mapping these values to those.”
Let’s explain those sqlite commands. “.headers on” adds headers to the display of output in the SQLite console. “.mode column” formats the output to be in a column layout. We took the time to configure these options to make the next two steps easier to read. Then, we get the contents of the articles and action_text_rich_texts tables. You should see the same number of records in each, and you should see the action_text_rich_texts table populated with data that matches the article record to which it corresponds.
As the console output shows, Article.first.body did a few things. First, it loaded the Article object. Then, it loaded its corresponding ActionText::RichText object for the body attribute. Then it loaded an action_text template! Lastly, it returned the body as an instance of the ActionText::RichText class. Notice the body value of the ActionText::RichText object—it’s actually an instance of the ActionText::Content class. We won’t dig even further at this point, but just know this means our simple Article#body attribute now has a lot of new behavior attached to it.
Now that we know our Article model is fetching its body content from Action Text’s database tables, let’s add a database migration to remove the body column from the articles table. This isn’t necessary, but since we aren’t using it (and we’re sure we don’t need its data anymore), we’ll remove it to keep things tidy.
Migration to Remove the Body Column from the Articles Tablehttps://gist.github.com/nicedawg/176b87f36550f8269fa4afcf73f4502e
This migration will remove the body column from the articles table when migrating upward and will add the column back when calling rails db:rollback. However, when rolling back, it won’t restore the data. We’re okay with that, because we know we don’t need it, but be aware that extra steps would be necessary if that were not the case.
At this point, we feel confident that our Article model is correctly integrated with Action Text, so let’s continue enhancing our blog application.
Updating the Article View
After having updated our Article model to use Action Text for its body, if you were try to load the root path (or /articles path) of our application, you would see an error: “NoMethodError in Articles#index: undefined method `strip' for #<ActionText::RichText:0x00007ff6f6b51478>”. We shouldn’t be too surprised; we changed what type of object article.body returns, so we have to deal with it a little differently.
Displaying Action Text Content in _article.html.erbhttps://gist.github.com/nicedawg/2c3d1d2f14899ffb944099879935857c
After updating app/views/articles/_article.html.erb, we can now load the various pages that display an article body without error. Nothing really looks different than before, but that’s good! Our article bodies don’t have any HTML in them. Yet.
Updating the Article Form
Now, almost everything’s in place. The support system for articles having HTML in their body fields is there; we just need to update the form to use a WYSIWYG editor.
Updating the Article Form to Use the Trix Editor for Its Body Input https://gist.github.com/nicedawg/d76b2b3ad4e4b1dd5eefd2af2f1a5a3f

Article form using Trix as WYSIWYG editor for the body field
Try it out! Restart your Rails server to be sure all of our new code is loaded, and use the article form to add content to your body, using Trix’s toolbar to make some of your content bold and add links, lists, and other formatting. Then view the article, and see your fancy formatting in the body tag.
Cleaning Up N+1 Queries
In an earlier section in this chapter, we described what happened when we added has_rich_text :body to our Article model. One of the benefits we described was the inclusion of scopes to help us deal with N+1 queries.
Too Many SQL Queries for Loading Articleshttps://gist.github.com/nicedawg/70ba78c747b96fa0ec4d7a924b760586
To help make the output easier to read, we truncated and omitted extremely long lines. But scanning the output, we see a pattern; over and over again, we query the same three tables: users, active_storage_attachments, and action_text_rich_texts.
At this point, we only have a few articles, so the performance hit of making at least three queries may not be noticeable. But each article in our database would result in at least three queries being executed, so imagine if we had 50 articles or 100. This doesn’t scale, so we need to deal with these N+1 queries we’ve accumulated along the way.
Fixing N+1 Queries in ArticlesControllerhttps://gist.github.com/nicedawg/e47509b6a42e7732475c0db89bf40b65
Instead of simply calling Article.all, we add a few things to eliminate our N+1 queries. First, we use .includes(:user) to hint to Active Record that we want to know the user which each article belongs to. (:user is the name of the relevant association, so that’s what we provide to includes. Next, we chain the with_rich_text_body scope. This scope was added automatically to our Article class when we added has_rich_content :body and similarly hints to Rails we want to efficiently load the relevant action_text_rich_texts records for each article. Lastly, we chain the with_attached_cover_image scope, which Active Storage automatically added to our class when we added has_one_attached :cover_image in the previous chapter.
No More N+1 Queries When Loading Articleshttps://gist.github.com/nicedawg/cfe9422f14763cbf831a224d7c74b8c5
Again, note that we truncated and omitted some extremely long lines for clarity. But look again for the SELECT statements. Instead of dozens of SELECT statements, there are only a few! This is a good sign that our optimizations were effective.
Summary
In this chapter, we ran the action_text:install command and investigated what changes it made to our app in order to support using Trix as a WYSIWYG editor. Then, we enhanced our blog application by allowing users to create HTML in their articles’ bodies without needing to learn HTML.
While we ended up covering most of what you need to know to work with Action Text, https://edgeguides.rubyonrails.org/action_text_overview.html is a great resource for future reference.