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 theapplication.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.