More at rubyonrails.org:

1 What is Action Text?

Action Text facilitates the handling and display of rich text content. Rich text content is text that includes formatting elements such as bold, italics, colors, and hyperlinks, providing a visually enhanced and structured presentation beyond plain text. It allows us to create rich text content, store it in a table, then attach it to any of our models.

Action Text includes a WYSIWYG editor called Trix, which is used in web applications to provide users with a user-friendly interface for creating and editing rich text content. It handles everything from providing enriching capabilities like the formatting of text, adding links or quotes, embedding images, and much much more. See the Trix editor website for examples.

The rich text content generated by the Trix editor is saved in its own RichText model that can be associated with any existing Active Record model in the application. In addition, any embedded images (or other attachments) can be automatically stored using Active Storage (which is added as a dependency) and associated with that RichText model. When it's time to render content, Action Text processes the content by sanitizing it first so that it's safe to embed directly into the page's HTML.

Most WYSIWYG editors are wrappers around HTML’s contenteditable and execCommand APIs. These APIs were designed by Microsoft to support live editing of web pages in Internet Explorer 5.5. They were eventually reverse-engineered and copied by other browsers. Consequently, these APIs were never fully specified or documented, and because WYSIWYG HTML editors are enormous in scope, each browser's implementation has its own set of bugs and quirks. Hence, JavaScript developers are often left to resolve the inconsistencies.

Trix sidesteps these inconsistencies by treating contenteditable as an I/O device: when input makes its way to the editor, Trix converts that input into an editing operation on its internal document model, then re-renders that document back into the editor. This gives Trix complete control over what happens after every keystroke and avoids the need to use execCommand and the inconsistencies that come along with it.

2 Installation

To install Action Text and start working with rich text content, run:

$ bin/rails action_text:install

It will do the following:

  • Installs the JavaScript packages for trix and @rails/actiontext and adds them to the application.js.
  • Adds the image_processing gem for analysis and transformations of the embedded images and other attachments with Active Storage. Please refer to the Active Storage Overview guide for more information about it.
  • Adds migrations to create the following tables that store rich text content and attachments: action_text_rich_texts, active_storage_blobs, active_storage_attachments, active_storage_variant_records.
  • Creates actiontext.css which includes all Trix styles and overrides.
  • Adds the default view partials _content.html and _blob.html to render Action Text content and Active Storage attachment (aka blob) respectively.

Thereafter, executing the migrations will add the new action_text_* and active_storage_* tables to your app:

$ bin/rails db:migrate

When the Action Text installation creates the action_text_rich_texts table, it uses a polymorphic relationship so that multiple models can add rich text attributes. This is done through the record_type and record_id columns, which store the ClassName of the model, and ID of the record, respectively.

With polymorphic associations, a model can belong to more than one other model, on a single association. Read more about it in the Active Record Associations guide.

Hence, if your models containing Action Text content use UUID values as identifiers, then all models that use Action Text attributes will need to use UUID values for their unique identifiers. The generated migration for Action Text will also need to be updated to specify type: :uuid for the record references line.

t.references :record, null: false, polymorphic: true, index: false, type: :uuid

3 Creating Rich Text Content

This section explores some of the configurations you'll need to follow to create rich text.

The RichText record holds the content produced by the Trix editor in a serialized body attribute. It also holds all the references to the embedded files, which are stored using Active Storage. This record is then associated with the Active Record model which desires to have rich text content. The association is made by placing the has_rich_text class method in the model that you’d like to add rich text to.

# app/models/article.rb
class Article < ApplicationRecord
  has_rich_text :content
end

There's no need to add the content column to your Article table. has_rich_text associates the content with the action_text_rich_texts table that has been created, and links it back to your model. You also may choose to name the attribute to be something different from content.

Once you have added the has_rich_text class method to the model, you can then update your views to make use of the rich text editor (Trix) for that field. To do so, use a rich_textarea for the form field.

<%# app/views/articles/_form.html.erb %>
<%= form_with model: article do |form| %>
  <div class="field">
    <%= form.label :content %>
    <%= form.rich_textarea :content %>
  </div>
<% end %>

This will display a Trix editor that provides the functionality to create and update your rich text accordingly. Later we'll go into details about how to update the styles for the editor.

Finally, to ensure that you can accept updates from the editor, you will need to permit the referenced attribute as a parameter in the relevant controller:

class ArticlesController < ApplicationController
  def create
    article = Article.create! params.expect(article: [:title, :content])
    redirect_to article
  end
end

If the need arises to rename classes that utilize has_rich_text, you will also need to update the polymorphic type column record_type in the action_text_rich_texts table for the respective rows.

Since Action Text depends on polymorphic associations, which, in turn, involve storing class names in the database, it's crucial to keep the data in sync with the class names used in your Ruby code. This synchronization is essential to maintain consistency between the stored data and the class references in your codebase.

4 Rendering Rich Text Content

Instances of ActionText::RichText can be directly embedded into a page because they have already sanitized their content for a safe render. You can display the content as follows:

<%= @article.content %>

ActionText::RichText#to_s safely transforms RichText into an HTML String. On the other hand ActionText::RichText#to_plain_text returns a string that is not HTML safe and should not be rendered in browsers. You can learn more about Action Text's sanitization process in the ActionText::RichText documentation.

If there's an attached resource within content field, it might not show properly unless you have the necessary dependencies for Active Storage installed.

5 Customizing the Rich Text Content Editor (Trix)

There may be times when you want to update the presentation of the editor to meet your stylistic requirements, this section guides on how to do that.

5.1 Removing or Adding Trix Styles

By default, Action Text will render rich text content inside an element with the .trix-content class. This is set in app/views/layouts/action_text/contents/_content.html.erb. Elements with this class are then styled by the trix stylesheet.

If you’d like to update any of the trix styles, you can add your custom styles in app/assets/stylesheets/actiontext.css, which includes both the full set of styles for Trix and the overrides needed for Action Text.

5.2 Customizing the Editor Container

To customize the HTML container element that's rendered around rich text content, edit the app/views/layouts/action_text/contents/_content.html.erb layout file created by the installer:

<%# app/views/layouts/action_text/contents/_content.html.erb %>
<div class="trix-content">
  <%= yield %>
</div>

5.3 Customizing HTML for Embedded Images and Attachments

To customize the HTML rendered for embedded images and other attachments (known as blobs), edit the app/views/active_storage/blobs/_blob.html.erb template created by the installer:

<%# app/views/active_storage/blobs/_blob.html.erb %>
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
  <% if blob.representable? %>
    <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
  <% end %>

  <figcaption class="attachment__caption">
    <% if caption = blob.try(:caption) %>
      <%= caption %>
    <% else %>
      <span class="attachment__name"><%= blob.filename %></span>
      <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
    <% end %>
  </figcaption>
</figure>

6 Attachments

Currently, Action Text supports attachments that are uploaded through Active Storage as well as attachments that are linked to a Signed GlobalID.

6.1 Active Storage

When uploading an image within your rich text editor, it uses Action Text which in turn uses Active Storage. However, Active Storage has some dependencies which are not provided by Rails. To use the built-in previewers, you must install these libraries.

Some, but not all of these libraries are required and they are dependent on the kind of uploads you are expecting within the editor. A common error that users encounter when working with Action Text and Active Storage is that images do not render correctly in the editor. This is usually due to the libvips dependency not being installed.

6.1.1 Attachment Direct Upload JavaScript Events

Event name Event target Event data (event.detail) Description
direct-upload:start <input> {id, file} A direct upload is starting.
direct-upload:progress <input> {id, file, progress} As requests to store files progress.
direct-upload:error <input> {id, file, error} An error occurred. An alert will display unless this event is canceled.
direct-upload:end <input> {id, file} A direct upload has ended.

6.2 Signed GlobalID

In addition to attachments uploaded through Active Storage, Action Text can also embed anything that can be resolved by a Signed GlobalID.

A Global ID is an app-wide URI that uniquely identifies a model instance: gid://YourApp/Some::Model/id. This is helpful when you need a single identifier to reference different classes of objects.

When using this method, Action Text requires attachments to have a signed global ID (sgid). By default, all Active Record models in a Rails app mix in the GlobalID::Identification concern, so they can be resolved by a signed global ID and are therefore ActionText::Attachable compatible.

Action Text references the HTML you insert on save so that it can re-render with up-to-date content later on. This makes it so that you can reference models and always display the current content when those records change.

Action Text will load up the model from the global ID and then render it with the default partial path when you render the content.

An Action Text Attachment can look like this:

<action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>

Action Text renders embedded <action-text-attachment> elements by resolving their sgid attribute of the element into an instance. Once resolved, that instance is passed along to a render helper. As a result, the HTML is embedded as a descendant of the <action-text-attachment> element.

To be rendered within Action Text <action-text-attachment> element as an attachment, we must include the ActionText::Attachable module, which implements #to_sgid(**options) (made available through the GlobalID::Identification concern).

You can also optionally declare #to_attachable_partial_path to render a custom partial path and #to_missing_attachable_partial_path for handling missing records.

An example can be found here:

class Person < ApplicationRecord
  include ActionText::Attachable
end

person = Person.create! name: "Javan"
html = %Q(<action-text-attachment sgid="#{person.attachable_sgid}"></action-text-attachment>)
content = ActionText::Content.new(html)
content.attachables # => [person]

6.3 Rendering an Action Text Attachment

The default way that an <action-text-attachment> is rendered is through the default path partial.

To illustrate this further, let’s consider a User model:

# app/models/user.rb
class User < ApplicationRecord
  has_one_attached :avatar
end

user = User.find(1)
user.to_global_id.to_s #=> gid://MyRailsApp/User/1
user.to_signed_global_id.to_s #=> BAh7CEkiCG…

We can mix GlobalID::Identification into any model with a .find(id) class method. Support is automatically included in Active Record.

The above code will return our identifier to uniquely identify a model instance.

Next, consider some rich text content that embeds an <action-text-attachment> element that references the User instance's signed GlobalID:

<p>Hello, <action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>.</p>

Action Text uses the "BAh7CEkiCG…" String to resolve the User instance. It then renders it with the default partial path when you render the content.

In this case, the default partial path is the users/user partial:

<%# app/views/users/_user.html.erb %>
<span><%= image_tag user.avatar %> <%= user.name %></span>

Hence, the resulting HTML rendered by Action Text would look something like:

<p>Hello, <action-text-attachment sgid="BAh7CEkiCG…"><span><img src="..."> Jane Doe</span></action-text-attachment>.</p>

6.4 Rendering a Different Partial for the action-text-attachment

To render a different partial for the attachable, define User#to_attachable_partial_path:

class User < ApplicationRecord
  def to_attachable_partial_path
    "users/attachable"
  end
end

Then declare that partial. The User instance will be available as the user partial-local variable:

<%# app/views/users/_attachable.html.erb %>
<span><%= image_tag user.avatar %> <%= user.name %></span>

6.5 Rendering a Partial for an Unresolved Instance or Missing action-text-attachment

If Action Text is unable to resolve the User instance (for example, if the record has been deleted), then a default fallback partial will be rendered.

To render a different missing attachment partial, define a class-level to_missing_attachable_partial_path method:

class User < ApplicationRecord
  def self.to_missing_attachable_partial_path
    "users/missing_attachable"
  end
end

Then declare that partial.

<%# app/views/users/missing_attachable.html.erb %>
<span>Deleted user</span>

6.6 Attachable via API

If your architecture does not follow the traditional Rails server-side rendered pattern, then you may perhaps find yourself with a backend API (for example, using JSON) that will need a separate endpoint for uploading files. The endpoint will be required to create an ActiveStorage::Blob and return its attachable_sgid:

{
  "attachable_sgid": "BAh7CEkiCG…"
}

Thereafter, you can take the attachable_sgid and insert it in rich text content within your frontend code using the <action-text-attachment> tag:

<action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>

7 Miscellaneous

7.1 Avoiding N+1 Queries

If you wish to preload the dependent ActionText::RichText model, assuming your rich text field is named content, you can use the named scope:

Article.all.with_rich_text_content # Preload the body without attachments.
Article.all.with_rich_text_content_and_embeds # Preload both body and attachments.


Back to top