action-text
This skill should be used when the user asks about "rich text", "Action Text", "Trix editor", "WYSIWYG", "has_rich_text", "content editing", "embedded attachments", "formatted text", "text editor", or needs guidance on implementing rich text editing in Rails applications.
When & Why to Use This Skill
This Claude skill provides a comprehensive implementation guide for Action Text in Ruby on Rails, enabling developers to seamlessly integrate rich text editing and the Trix editor. It covers the entire lifecycle of content editing, from initial setup and model configuration to advanced customization, attachment handling via Active Storage, and performance optimization through eager loading.
Use Cases
- Implementing a WYSIWYG editor in Rails applications for blogs, CMS platforms, or comment sections.
- Managing and displaying embedded attachments like images and files within rich text content using Active Storage.
- Customizing the Trix editor toolbar to add specialized buttons or remove default tools for a tailored user experience.
- Securing sensitive user data by implementing encrypted rich text fields in Rails models.
- Optimizing database performance by using eager loading scopes to prevent N+1 queries when fetching rich text content.
| name | action-text |
|---|---|
| description | This skill should be used when the user asks about "rich text", "Action Text", "Trix editor", "WYSIWYG", "has_rich_text", "content editing", "embedded attachments", "formatted text", "text editor", or needs guidance on implementing rich text editing in Rails applications. |
| version | 1.0.0 |
Action Text
Comprehensive guide to rich text content with the Trix editor in Rails.
Setup
rails action_text:install
rails db:migrate
This creates:
active_storagetables (if not present)action_text_rich_textstable- Imports Trix editor and styles
JavaScript Setup
// app/javascript/application.js
import "trix"
import "@rails/actiontext"
Stylesheet Setup
// app/assets/stylesheets/application.scss
@import "trix/dist/trix";
// Or in application.css
//= require trix
//= require actiontext
Model Configuration
Basic Rich Text
class Article < ApplicationRecord
has_rich_text :content
end
Multiple Rich Text Fields
class Article < ApplicationRecord
has_rich_text :content
has_rich_text :summary
has_rich_text :notes
end
Encrypted Rich Text
class Article < ApplicationRecord
has_rich_text :content, encrypted: true
end
Form Integration
Basic Form
<%= form_with model: @article do |form| %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :content %>
<%= form.rich_text_area :content %>
</div>
<%= form.submit %>
<% end %>
With Placeholder
<%= form.rich_text_area :content, placeholder: "Write your article here..." %>
With Custom Class
<%= form.rich_text_area :content, class: "custom-editor", data: { controller: "editor" } %>
Controller Setup
Strong Parameters
class ArticlesController < ApplicationController
def create
@article = Article.new(article_params)
# ...
end
private
def article_params
params.require(:article).permit(:title, :content)
end
end
Displaying Content
Basic Display
<%# Renders as HTML %>
<%= @article.content %>
<%# With wrapper div %>
<div class="prose">
<%= @article.content %>
</div>
Plain Text
<%# Plain text version %>
<%= @article.content.to_plain_text %>
<%# Truncated plain text %>
<%= truncate(@article.content.to_plain_text, length: 200) %>
Checking for Content
<% if @article.content.present? %>
<%= @article.content %>
<% else %>
<p class="empty">No content yet.</p>
<% end %>
<%# Or %>
<% if @article.content.blank? %>
<p>Write something!</p>
<% end %>
Attachments
Image Attachments
Action Text automatically handles image attachments through Active Storage:
# Images are automatically embedded when pasted or dragged into editor
# They're stored via Active Storage
Custom Attachments
# app/models/user.rb
class User < ApplicationRecord
include ActionText::Attachable
def to_trix_content_attachment_partial_path
"users/mention"
end
end
<%# app/views/users/_mention.html.erb %>
<span class="mention">@<%= user.name %></span>
Embedding Attachments Programmatically
# Attach a user mention
article.content = ActionText::Content.new("<div>Hello #{user.attachable_sgid}</div>")
# Using the helper
article.update(content: "<div>Check out #{ActionText::Attachment.from_attachable(user)}</div>")
Attachment Gallery
<%# Display all attachments from rich text %>
<% @article.content.attachments.each do |attachment| %>
<% if attachment.attachable.is_a?(ActiveStorage::Blob) %>
<%= image_tag attachment.attachable.representation(resize_to_limit: [200, 200]) %>
<% end %>
<% end %>
Customizing Trix
Toolbar Configuration
// app/javascript/controllers/trix_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
// Remove unwanted toolbar buttons
this.element.addEventListener("trix-initialize", () => {
const toolbar = this.element.previousElementSibling
toolbar.querySelector(".trix-button-group--file-tools")?.remove()
})
}
}
<div data-controller="trix">
<%= form.rich_text_area :content %>
</div>
Custom Toolbar
// Remove specific buttons
document.addEventListener("trix-initialize", (event) => {
const toolbar = event.target.toolbarElement
// Remove file attachment button
toolbar.querySelector('[data-trix-action="attachFiles"]')?.remove()
// Remove heading button
toolbar.querySelector('[data-trix-attribute="heading1"]')?.remove()
})
Adding Custom Buttons
// Add custom button to toolbar
document.addEventListener("trix-initialize", (event) => {
const toolbar = event.target.toolbarElement
const buttonGroup = toolbar.querySelector(".trix-button-group--block-tools")
const button = document.createElement("button")
button.setAttribute("type", "button")
button.setAttribute("class", "trix-button")
button.setAttribute("data-trix-attribute", "highlight")
button.textContent = "Highlight"
buttonGroup.appendChild(button)
})
// Define the attribute
Trix.config.textAttributes.highlight = {
tagName: "mark",
inheritable: true
}
Custom Styles
// Customize Trix appearance
trix-editor {
min-height: 300px;
padding: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
&:focus {
border-color: #3b82f6;
outline: none;
}
}
trix-toolbar {
background: #f9fafb;
border-bottom: 1px solid #ddd;
padding: 0.5rem;
}
// Style the rendered content
.trix-content {
h1 { font-size: 1.5rem; margin-top: 1rem; }
blockquote {
border-left: 3px solid #ddd;
padding-left: 1rem;
color: #666;
}
pre {
background: #f4f4f5;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
}
Event Handling
JavaScript Events
// Listen for content changes
document.addEventListener("trix-change", (event) => {
const editor = event.target
console.log("Content changed:", editor.value)
})
// Before paste
document.addEventListener("trix-before-paste", (event) => {
// Modify paste behavior
})
// Before file accept
document.addEventListener("trix-file-accept", (event) => {
// Validate file
const acceptedTypes = ["image/jpeg", "image/png", "image/gif"]
if (!acceptedTypes.includes(event.file.type)) {
event.preventDefault()
alert("Only images are allowed!")
}
// Limit file size (5MB)
if (event.file.size > 5 * 1024 * 1024) {
event.preventDefault()
alert("File too large!")
}
})
// After file attached
document.addEventListener("trix-attachment-add", (event) => {
const attachment = event.attachment
if (attachment.file) {
uploadFile(attachment)
}
})
Stimulus Controller
// app/javascript/controllers/rich_text_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["editor", "counter"]
connect() {
this.updateCounter()
}
change(event) {
this.updateCounter()
this.autoSave()
}
updateCounter() {
const text = this.editorTarget.editor.getDocument().toString()
this.counterTarget.textContent = `${text.length} characters`
}
autoSave() {
clearTimeout(this.saveTimer)
this.saveTimer = setTimeout(() => {
this.save()
}, 2000)
}
save() {
// Auto-save logic
}
}
Querying Rich Text
Search Content
# Find articles containing text
Article.joins(:rich_text_content)
.where("action_text_rich_texts.body LIKE ?", "%search term%")
# Scope for searching
class Article < ApplicationRecord
has_rich_text :content
scope :search_content, ->(term) {
joins(:rich_text_content)
.where("action_text_rich_texts.body LIKE ?", "%#{term}%")
}
end
Eager Loading
# Avoid N+1 queries
@articles = Article.all.with_rich_text_content
# Multiple rich text fields
@articles = Article.all.with_rich_text_content.with_rich_text_summary
# With embedded images
@articles = Article.all.with_rich_text_content_and_embeds
Testing
Minitest
# test/models/article_test.rb
require "test_helper"
class ArticleTest < ActiveSupport::TestCase
test "has rich text content" do
article = Article.new(title: "Test", content: "<p>Hello World</p>")
assert article.content.present?
assert_includes article.content.to_plain_text, "Hello World"
end
test "content with attachment" do
article = articles(:one)
blob = active_storage_blobs(:image)
article.content.body.attachables << blob
article.save!
assert_equal 1, article.content.body.attachments.count
end
end
System Tests
# test/system/articles_test.rb
require "application_system_test_case"
class ArticlesTest < ApplicationSystemTestCase
test "creating article with rich text" do
visit new_article_path
fill_in "Title", with: "My Article"
# Fill in Trix editor
find("trix-editor").click.set("This is rich text content")
click_button "Create Article"
assert_text "Article was successfully created"
assert_text "This is rich text content"
end
end
Security
Sanitization
Action Text automatically sanitizes content. Customize allowed tags:
# config/initializers/action_text.rb
Rails.application.config.after_initialize do
ActionText::ContentHelper.allowed_tags = %w[
strong em del a h1 h2 h3 h4 blockquote pre code ul ol li
]
ActionText::ContentHelper.allowed_attributes = %w[
href class data-*
]
end
Content Security
# Sanitize on output (additional layer)
<%= sanitize @article.content.to_s, tags: %w[p strong em a], attributes: %w[href] %>