1 はじめに

Action Textは、リッチテキストコンテンツを手軽に処理・表示する機能です。リッチテキストコンテンツは、太字・斜体・色・ハイパーリンクなどの書式設定要素を含むテキストであり、プレーンテキストよりも豊かに表示可能で、構造化されたプレゼンテーションを提供します。Action Textを利用することで、リッチテキストコンテンツを作成してテーブルに保存することも、任意のモデルに添付することも可能になります。

Trixエディタが生成するリッチテキストコンテンツは独自のRichTextモデルに保存され、このモデルはアプリケーションの既存のあらゆるActive Recordモデルと関連付けられます。 あらゆる埋め込み画像(およびその他の添付ファイル)は自動的にActive Storageに保存され、includeされたRichTextモデルに関連付けられます。

Action Textには、Trixと呼ばれるWYSIWYGエディタが含まれています。Trixはリッチテキストコンテンツの作成・編集用の使いやすいインターフェイスをユーザーに提供するためにWebアプリケーションで利用され、テキストの書式設定、リンクや引用の追加、画像埋め込みなど多くの機能が使えるようになります。Trixエディタの利用例について詳しくはTrixエディタのWebサイトを参照してください。

Trixエディタで生成されたリッチテキストコンテンツは、アプリケーションにある既存のActive Recordモデルに関連付け可能な独自のRichTextモデルに保存されます。さらに、埋め込み画像(またはその他の添付ファイル)は、Active Storage(依存関係として追加されます)自動的に保存されてRichTextモデルに関連付けられます。コンテンツをレンダリングするとき、Action Textが最初にコンテンツをサニタイズしてから処理するので、ページのHTMLに直接埋め込んでも安全です。

WYSIWYGエディタのほとんどは、HTMLのcontenteditableexecCommandAPIのラッパーです。これらのAPIは、Internet Explorer 5.5でWebページのライブ編集をサポートするためにMicrosoftによって設計されました。これらは最終的にリバースエンジニアリングされて他のブラウザにコピーされました。その結果、これらのAPIは完全な形では仕様化されておらず、ドキュメント化もされていません。また、WYSIWYG HTMLエディタが扱う範囲が広大であるため、ブラウザの実装ごとに独自のバグや癖が存在します。したがって、この不一致は多くの場合JavaScript開発者によって解決されなければなりませんでした。

Trixは、contenteditableをI/Oデバイスとして扱うことで、こうした不一致を回避します。入力がエディタに送信されると、Trixはその入力を編集用に変換して内部ドキュメントモデルに対する操作を実行してから、そのドキュメントをエディタに再レンダリングします。これにより、Trixは振る舞いをキーストローク単位で完全に制御できるようになり、execCommandへの依存とそれに伴う不一致を回避できます。

2 インストール

Action Textをインストールしてリッチテキストコンテンツを扱えるようにするには、以下を実行します。

$ bin/rails action_text:install

上を実行すると、以下が行われます。

  • trix@rails/actiontextで利用するJavaScriptパッケージをインストールして、application.jsファイルに追加します。
  • image_processing gem(Active Storageで埋め込み画像などの添付ファイルの分析・変換を行う)を追加します。詳しくはActive Storageの概要ガイドを参照してください。
  • リッチテキストコンテンツや添付ファイルを保存するために以下のテーブルを作成するマイグレーションファイルを追加します。
    • action_text_rich_texts
    • active_storage_blobs
    • active_storage_attachments
    • active_storage_variant_records
  • actiontext.cssを作成します。ここにはTrixスタイルシートも含まれます。
  • Action Textコンテンツをレンダリングするためのビューパーシャル_content.htmlと、Active Storageの添付ファイル(blob)をレンダリングするための_blob.htmlを追加します。

続いて以下のようにマイグレーションを実行すると、アプリケーションにaction_text_*テーブルとactive_storage_*テーブルが追加されます。

$ bin/rails db:migrate

Action Textのインストールでaction_text_rich_textsテーブルを作成する場合、ポリモーフィックリレーションシップが使われるため、複数のモデルでリッチテキスト属性を追加可能になります。これは、モデルのClassNameを保存するrecord_typeカラムとレコードのIDを保存するrecord_idカラムを通じて行われます。

ポリモーフィック関連付けを利用すると、1個の関連付けでモデルを複数の他のモデルに従属させることが可能になります。詳しくはActive Recordの関連付けガイドを参照してください。

したがって、Action Textのコンテンツを含むモデルが識別子としてUUID値を利用する場合は、Action Textの属性を使うすべてのモデルでもUUID値を一意の識別子として使わなければなりません。Action Text用に生成したマイグレーションでも、以下のようにレコードのreferences行にtype: :uuidを指定する形で更新する必要があります。

t.references :record, null: false, polymorphic: true, index: false, type: :uuid

3 リッチテキストコンテンツを作成する

本セクションでは、リッチテキストを作成するときに従う必要があるいくつかの設定について解説します。

RichTextレコードは、Trixエディタによって生成されたコンテンツをシリアライズbody属性に保持します。ここには、Active Storageによって保存される埋め込みファイルへのすべての参照も保持されます。このレコードは、リッチテキストコンテンツを必要とするActive Recordモデルに関連付けられます。この関連付けを行うには、リッチテキストを追加するモデルで以下のようにhas_rich_textクラスメソッドを配置します。

# app/models/article.rb
class Article < ApplicationRecord
  has_rich_text :content
end

Articleモデルのテーブルにcontentフィールドを追加する必要はありません(has_rich_textクラスメソッドによって、作成済みのaction_text_rich_textsテーブルに関連付けられ、モデルにリンクされます)。また、属性名をcontent以外に変更することも可能です。

has_rich_textクラスメソッドをモデルに追加したら、そのフィールドでリッチテキストエディタ(Trix)を利用できるようにビューを更新します。これを行うには、ビューのフォームフィールドで以下のようにrich_textareaメソッドを使います。

<%# app/views/articles/_form.html.erb %>
<%= form_with model: article do |form| %>
  <div class="field">
    <%= form.label :content %>
    <%= form.rich_textarea :content %>
  </div>
<% end %>

これによりTrixエディタが表示され、リッチテキストを作成・更新する機能が提供されます。詳しくはエディタのスタイルを更新する方法で後述します。

最後に、エディタからの更新を受け付け可能にするため、参照する属性を以下のようにpermitでパラメータとして関連コントローラ内で許可する必要があります。

class ArticlesController < ApplicationController
  def create
    article = Article.create! params.expect(article: [:title, :content])
    redirect_to article
  end
end

has_rich_textを利用するクラスの名前を変更する必要が生じた場合は、action_text_rich_textsテーブル内の対応するすべての行でポリモーフィック型record_typeカラムも更新しなければなりません。

Action Textが依存しているポリモーフィック関連付けでは、クラス名をデータベースに保存する必要があるため、Rubyコードで使われるクラス名とデータがずれないよう常に同期を保つことが重要です。この同期は、保存したデータとコードベース内のクラス参照との一貫性を維持するうえで不可欠です。

4 リッチテキストコンテンツをレンダリングする

ActionText::RichTextのインスタンスは、安全なレンダリングのためにコンテンツがサニタイズ済みなので、ビューのページに直接埋め込み可能です。コンテンツは以下のように表示できます。

<%= @article.content %>

ActionText::RichText#to_sメソッドはRichTextをHTML安全な文字列に変換しますが、ActionText::RichText#to_plain_textはHTML安全ではない文字列を返すため、ブラウザでレンダリングすべきではありません。Action Textのサニタイズプロセスについて詳しくは、APIドキュメントのActionText::RichTextクラスを参照してください。

contentフィールドに添付(attached)リソースが存在する場合は、リソースの種別に応じてActive Storageで必要な依存関係をインストールしておかないと正しく表示されない可能性があります。

5 リッチテキストコンテンツエディタ(Trix)をカスタマイズする

スタイル上の要件を満たすためにエディタの表示を更新したい場合があります。本セクションでは、その方法について解説します。

5.1 Trixのスタイルを追加・削除する

デフォルトでは、Action TextはCSSの.trix-contentクラスを宣言した要素内でリッチテキストコンテンツをレンダリングします。これはapp/views/layouts/action_text/contents/_content.html.erbで設定されます。このクラスの要素のスタイルは、Trixのスタイルシートによって設定されます。

Trixのスタイルのいずれかを更新したい場合は、app/assets/stylesheets/actiontext.cssにカスタムスタイルを追加できます。ここには、Trix用のスタイルシートの完全なセットと、Action Textで必要なオーバーライドの両方が含まれています。

5.2 エディタコンテナをカスタマイズする

リッチテキストコンテンツの周囲にレンダリングされるHTMLコンテナ要素をカスタマイズするには、インストーラが作成した以下のapp/views/layouts/action_text/contents/_content.html.erbレイアウトファイルを編集します。

<%# app/views/layouts/action_text/contents/_content.html.erb %>
<div class="trix-content">
  <%= yield %>
</div>

5.3 埋め込み画像や添付ファイルのHTMLをカスタマイズする

埋め込み画像やその他の添付ファイル(いわゆるblob)に対してレンダリングされるHTMLをカスタマイズするには、インストーラが作成するapp/views/active_storage/blobs/_blob.html.erbテンプレートを以下のように編集します。

<%# 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 添付ファイル

現在のAction Textでは、Active Storage経由でアップロードされた添付ファイル(attachment)と、署名付きGlobalIDにリンクされた添付ファイルをサポートしています。

6.1 Active Storage

リッチテキストエディタ内で画像をアップロードするとAction Textが使われ、そしてActive Storageが使われます。ただしActive Storageで使われる依存関係の中には、デフォルトのRailsでは提供されていないものもあります。組み込みのプレビューアを利用するには、これらのライブラリを別途インストールしておく必要があります。

中には必須ではないライブラリもありますが、どのライブラリをインストールすべきかについては、エディタでのアップロードをサポートしたいファイルの種別によって異なります。ユーザーがAction TextやActive Storageを使うときによく遭遇するエラーは、エディタで画像が正しくレンダリングされないことです。このエラーは多くの場合、libvips依存関係がインストールされていないことが原因です。

6.1.1 添付ファイルのダイレクトアップロード用JavaScriptイベント
イベント名 イベントのターゲット イベントのデータ(event.detail 説明
direct-upload:start <input> {id, file} ダイレクトアップロードが開始中。
direct-upload:progress <input> {id, file, progress} ファイルの保存リクエストが進行中。
direct-upload:error <input> {id, file, error} エラーが発生。このイベントがキャンセルされない限りalertが表示される。
direct-upload:end <input> {id, file} ダイレクトアップロードが完了。

6.2 署名済みGlobalID

Action Textでは、Active Storage経由でアップロードした添付ファイルを埋め込めるだけでなく、署名済みグローバルIDで解決可能な任意のデータを埋め込むことも可能です。

グローバルIDは、gid://YourApp/Some::Model/idのような形式を取る、モデルのインスタンスをアプリ全体を通して一意に識別するURIです。グローバルIDは、クラスが異なる様々なオブジェクトを一意に参照する識別子が必要な場合に有用です。

グローバルIDを使う場合、Action Textは添付ファイルに対して署名済みグローバルID(sgid)を使うことを求めます。Railsアプリ内のすべてのActive Recordモデルは、デフォルトでGlobalID::Identificationのconcernをミックスインしているので、署名済みグローバルIDで解決可能であり、したがって、ActionText::Attachableと互換性があります。

Action Textは、保存時に挿入されたHTMLを参照し、後で最新のコンテンツで再レンダリングできるようにします。これにより、モデルの参照が可能となり、そのモデルのレコードが変更されたときに常に最新のコンテンツを表示できるようになります。

Action Textは、モデルをグローバルIDから読み込み、そのモデルのコンテンツをレンダリングする際にデフォルトのパーシャルパスを用います。

以下は、Action Textの添付ファイルの例です。

<action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>

Action Textは、要素のsgid属性をインスタンスに解決することで、埋め込まれた<action-text-attachment>要素をレンダリングします。解決が成功すると、そのインスタンスはレンダリングヘルパーに渡され、最終的なHTMLが<action-text-attachment>要素の子孫要素として埋め込まれます。

Action Textの<action-text-attachment>要素内で添付ファイルとしてレンダリングするには、#to_sgid(**options)メソッド(これはGlobalID::Identification concernを介して利用可能になります)を実装するActionText::Attachableモジュールをincludeする必要があります。

オプションとして、カスタムのパーシャルパスをレンダリングする#to_attachable_partial_pathメソッドや、欠落したレコードを処理する#to_missing_attachable_partial_pathメソッドも宣言できます。

以下はグローバルIDの利用例です。

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 Action Textの添付ファイルをレンダリングする

デフォルトでは、<action-text-attachment>はデフォルトのパーシャルパスを介してレンダリングされます。

以下のUserモデルを例に詳しく考えてみましょう。

# 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…

.find(id)クラスメソッドを持つ任意のモデルに、GlobalID::Identificationをミックスインできます。Active Recordでは、このサポートは自動的に含まれます。

上記のコードは、モデルのインスタンスを一意に識別するための識別子を返します。

次に、Userモデルのインスタンスにある署名済みグローバルIDを参照する<action-text-attachment>要素が埋め込まれたリッチテキストを考えてみましょう。

<p>Hello, <action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>.</p>

Action Textは、この"BAh7CEkiCG…"というStringを用いてUserインスタンスを解決し、次にデフォルトのパーシャルパスを用いてコンテンツをレンダリングします。

この場合、デフォルトのパーシャルパスはusers/userパーシャルになります。

<%# app/views/users/_user.html.erb %>
<span><%= image_tag user.avatar %> <%= user.name %></span>

これによって、Action Textで以下のHTMLがレンダリングされます。

<p>Hello, <action-text-attachment sgid="BAh7CEkiCG…"><span><img src="..."> Jane Doe</span></action-text-attachment>.</p>

6.4 action-text-attachmentで別のパーシャルをレンダリングする

別のパーシャルをレンダリングするには、以下のようにUser#to_attachable_partial_pathを定義します。

class User < ApplicationRecord
  def to_attachable_partial_path
    "users/attachable"
  end
end

次にそのパーシャルを宣言します。Userインスタンスは、パーシャル内のuserローカル変数でアクセスできます。

<%# app/views/users/_attachable.html.erb %>
<span><%= image_tag user.avatar %> <%= user.name %></span>

6.5 解決できなかったインスタンスやaction-text-attachmentが見つからないパーシャルをレンダリングする

Action TextがUserモデルのインスタンスを解決できない場合(レコードが削除されているなど)、デフォルトのフォールバック用パーシャルがレンダリングされます。

添付ファイルが見つからない場合にレンダリングするパーシャルを変更するには、以下のようにクラスレベルのto_missing_attachable_partial_pathメソッドを定義します。

class User < ApplicationRecord
  def self.to_missing_attachable_partial_path
    "users/missing_attachable"
  end
end

次にそのパーシャルを宣言します。

<%# app/views/users/missing_attachable.html.erb %>
<span>Deleted user</span>

6.6 ファイルアップロードAPIを独自に提供する

アプリケーションのアーキテクチャが伝統的なサーバーサイドレンダリングパターンに沿っていない場合は、バックエンドAPI(JSONを使うなど)で利用するファイルアップロード用のエンドポイントを自分で用意しなければならない場合があります。このエンドポイントはActiveStorage::Blobを作成し、以下のようにそのattachable_sgidを返す必要があります。

{
  "attachable_sgid": "BAh7CEkiCG…"
}

これで、attachable_sgidを取得してから<action-text-attachment>タグを用いてフロントエンドコード内のリッチテキストコンテンツに以下のように挿入できるようになります。

<action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>

7 その他

7.1 N+1クエリを回避する

依存するActionText::RichTextをプリロードしたい場合は、以下のように名前付きスコープを利用できます(リッチテキストフィールド名がcontentという前提)。

Message.all.with_rich_text_content            # 添付ファイルなしで本文をプリロードする
Message.all.with_rich_text_content_and_embeds # 本文と添付ファイルを両方プリロードする

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加