From ruby-on-rails
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.
npx claudepluginhub bastos/ruby-plugin-marketplace --plugin ruby-on-railsThis skill uses the workspace's default tool permissions.
Comprehensive guide to rich text content with the Trix editor in Rails.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Comprehensive guide to rich text content with the Trix editor in Rails.
rails action_text:install
rails db:migrate
This creates:
active_storage tables (if not present)action_text_rich_texts table// app/javascript/application.js
import "trix"
import "@rails/actiontext"
// app/assets/stylesheets/application.scss
@import "trix/dist/trix";
// Or in application.css
//= require trix
//= require actiontext
class Article < ApplicationRecord
has_rich_text :content
end
class Article < ApplicationRecord
has_rich_text :content
has_rich_text :summary
has_rich_text :notes
end
class Article < ApplicationRecord
has_rich_text :content, encrypted: true
end
<%= 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 %>
<%= form.rich_text_area :content, placeholder: "Write your article here..." %>
<%= form.rich_text_area :content, class: "custom-editor", data: { controller: "editor" } %>
class ArticlesController < ApplicationController
def create
@article = Article.new(article_params)
# ...
end
private
def article_params
params.require(:article).permit(:title, :content)
end
end
<%# Renders as HTML %>
<%= @article.content %>
<%# With wrapper div %>
<div class="prose">
<%= @article.content %>
</div>
<%# Plain text version %>
<%= @article.content.to_plain_text %>
<%# Truncated plain text %>
<%= truncate(@article.content.to_plain_text, length: 200) %>
<% 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 %>
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
# 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>
# 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>")
<%# 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 %>
// 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>
// 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()
})
// 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
}
// 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;
}
}
// 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)
}
})
// 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
}
}
# 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
# 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
# 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
# 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
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
# Sanitize on output (additional layer)
<%= sanitize @article.content.to_s, tags: %w[p strong em a], attributes: %w[href] %>