action-text

bastos's avatarfrom bastos

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.

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 10, 2026

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.
nameaction-text
descriptionThis 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.
version1.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_storage tables (if not present)
  • action_text_rich_texts table
  • 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>")
<%# 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] %>