Back to list
bastos

action-text

by bastos

Bastos' Claude Code Ruby Plugin Marketplace

0🍴 0📅 Jan 24, 2026

SKILL.md


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_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] %>

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon