TrixとTurbo Framesを使ってRailsで動的テーブルエディタを作成する方法
この記事では、Railsアプリケーション向けに基本的なActionTextテーブルエディタを実装する方法を解説します。以下の内容を学ぶことができます。
- ActionTextとTrixがアタッチメントをどのように扱うのか
- 独自の
Attachable型を実装し、それを活用して基本的なテーブルエディタを構築する方法 - Turbo Framesを使ってテーブルを編集する方法
- Turboが役に立つ一方で、同時に障害にもなり得るという事実
本記事は、2020年の優れたブログ記事「Adding Tables to ActionText With Stimulus.js」にインスピレーションを得ています。ただし、それはTurbo登場前のものでした。Turboがあれば話はかなりシンプルになると期待できます。
それでは始めましょう!
RailsにおけるActionTextアタッチメントの基礎
注意:このデモでは、TrixとTurbo Framesについてある程度の理解があることを前提としています。HotwireやTurbo Framesの基本を学びたい方は、「Get Started with Hotwire in Your Ruby on Rails App」という記事が参考になるでしょう。
コードのデモはGitHubリポジトリで確認できます。
ActionTextのドキュメントには次のように記載されています。
Action Textは、Railsにリッチテキストコンテンツと編集機能をもたらします。フォーマットからリンク、引用、リスト、埋め込み画像やギャラリーまで、あらゆる処理を担うTrixエディタが含まれています。
大まかに言えば、アタッチメントはActionTextのドキュメントモデルの一部です。Signed Global ID(SGID)で解決可能な任意のリソースに対して、カスタムテンプレートをレンダリングします。つまり、ActionTextは特定のSGIDへの参照を<action-text-attachment>要素として保存します。
ActionTextがこのような要素に遭遇すると、該当リソースのto_attachable_partial_pathメソッドを呼び出します。デフォルトでは、このメソッドはto_partial_pathに委譲されます。
プレビューとして、Tableの表現がHTMLとしてレンダリングされる際の見え方を紹介します。
ActionText Attachment APIに準拠するために、クラスが実装すべきことはわずか2つです。
GlobalID::Identificationをincludeしてto_sgidを実装する。デフォルトで、すべてのActiveRecord::Baseの子孫クラスはすでにこれを満たしています。ActionText::Attachableモジュールをincludeする。
ActionText::Attachableモジュールは、attachable_sgidとfrom_attachable_sgidメソッドを通じて、任意のモデルとSGIDを相互変換する標準的な手段を提供します。これについては後ほど活用します。
また、ファイルサイズや名前、コンテンツタイプといったアタッチメントのメタデータにアクセスするための便利なアクセサも備えています。
さらに、エディタおよびリッチテキストビューでアタッチメントをレンダリングするために使われるパーシャルのデフォルト配置場所も提供しています。
テーブルモデルの追加
ActionTextのAttachment APIを活用して、テーブルソリューションを実装していきます。そのためには、テーブルのデータを保持するカスタムモデルを作成し、Attachableをincludeする必要があります。テーブルデータは2次元配列として保持し、保存先にはシンプルなJSON(B)カラムを使用します。
まず、ActionTextを有効にした新しいRailsアプリを作成しましょう。
あまり創造力が湧かない日なので、タイトルとリッチテキストコンテンツを持つArticleモデルをスキャフォールドします。
ここで意外な落とし穴に注意!上記のインストールコマンドはCreateActionTextTablesというマイグレーションを生成するため、CreateActionTextTablesTableにリネームする必要があります。さらに、null: false, default: [["", ""], ["", ""]]を指定して、デフォルトで2x2のテーブルになるようにしましょう。
RailsのActionTextモデルにテーブルを追加する
実際にリッチテキストへテーブルを追加する前に、Trixのツールバーにパッチを当てる必要があります。
ここでは、TrixのtoolbarElementに手動でボタンを追加しています。このボタンを(まだ構築していない)trix-table Stimulusコントローラーに接続すると、ドキュメントにテーブルを挿入できるようになります。CSSでこのボタンにSVGアイコンを設定し、ついでにテーブル用のスタイルも整えておきましょう。
ご覧のとおり、「file-tools」グループに無事追加できました。

では、Turboの助けを借りてテーブルの追加と操作に戻りましょう。まずはcreateアクションを持つコントローラーが必要です。
このアクションは、冒頭で引用した「On Rails」ブログ記事からほぼそのまま借用したものです。クライアント側でアタッチメントを挿入するために必要なJSONを構築します。具体的には、SGIDと、後述するeditorパーシャルからレンダリングされたcontentを含みます。
続いて、対応するリソースフルなテーブルルートを設定に追加します。
さあ、いよいよ核心に迫ります。テーブルモデルを構築しましょう。まず、ActionText::Attachableをincludeし、関連するパーシャルパスを定義します。
まだテーブルの内容がどのように保存されるかは定義していないことに注意してください。データベースではJSON(B)カラムとして宣言しているため、任意の形式を選択できます。引用元のブログ記事から少し踏み込んで、ここでは2次元配列を採用します。そうすれば、contentに対して次のようなネストしたループを組むだけで済みます。
上記のパーシャルは、たとえばActionViewから要求されたときにレンダリングされます。次に、Trix内でインライン表示されるeditorパーシャルも用意する必要があります。
お気づきかもしれませんが、唯一の違いは、SGIDをDOM idとして使用してTurbo Frameでラップした点です。さらに、区切りブロックに行・列のインデックスを渡し、内側のDIVをcontenteditableにすることでインライン編集に備えています。これについては後ほど詳しく説明します。
次に、ツールバーのテーブルボタンを、先ほど作成したサーバー側のコントローラーアクションに接続します。そのために、まずRailsのrequest.jsライブラリをプロジェクトに導入しましょう。このライブラリは、CSRFトークンなどを適切に処理しながら、クライアントからのpostリクエスト管理を支援してくれます。
新しいTrixテーブルStimulusコントローラーの構築
すべての準備が整ったところで、新しいtrix-table Stimulusコントローラーを作成しましょう。ここで、ツールバーボタンが参照するattachTableアクションを実装します。
このアクションは、テーブルのcreateルートにPOSTリクエストを送信し、JSONレスポンスをTrixアタッチメントとして挿入します。これもOnRailsブログ記事からの借用ですが、非推奨となったrails-ujsの呼び出しを、より新しいrequest.jsライブラリに置き換えています。
次に、フォームのマークアップにコントローラーを追加して、アプリ内で実際に使用できるようにします。
Stimulus.jsの素晴らしいところは、form要素にdata属性を2つ追加するだけで目的の結果が得られる点です。これで、ワンクリックで記事のコンテンツにテーブルを追加できるようになりました。

Turbo Framesによるテーブル操作
テーブルアタッチメントを作成できるようになったので、次はコンテンツの操作に焦点を移しましょう。実は、Turbo Framesはここでほぼ理想的に適合します。
テーブルの行と列の追加・削除
テーブルの行と列を追加・削除するために、各操作に1つずつ、計4つのボタンからなるミニツールバーを作成します。button_toヘルパーを利用し、URLには対象テーブルのupdateルートを設定します。実行したい操作は、追加パラメータとして渡しましょう。
それに応じて、TablesControllerにも対応するコントローラーアクションを追加する必要があります。updateアクションがこれらの操作をモデルに委譲している点に注目してください。
テーブル構造の変更が保存された後は、テーブルの編集ビューへリダイレクトします。ここで同じeditorパーシャルがレンダリングされ、結果的に同じTurbo Frameが参照されます。これにより、Turboは一致するフレームを検出し、片方をもう片方に置き換えることができるのです。
次に、Tableモデルに未実装のコマンドを実装します。
特筆すべきは、2次元配列というシンプルなデータ構造のおかげで、add/remove_column/row系のメソッドが、列数と行数を変更する単なるプロキシで済んでいる点です。ここまで整えば、ボタンクリックだけでテーブルの構造を変更できるようになります。

テーブルセルの内容を編集する
行・列の数を変更できるだけでなく、セルの内容も編集したいところです。これを実現するために、再び引用元のブログ記事を大いに参考にしながら、Stimulusのテーブルエディタコントローラーを作成します。
updateCellメソッドは、セルが編集されるたびにPATCHリクエストを送信し、行と列のインデックスをパラメータとして渡します。あとは、これをDOMに接続するだけです。
もちろん、サーバー側のTablesControllerも、この操作を処理する仕組みが必要です。幸い、この簡略化された概念実証では、条件分岐にもう1つのブランチを追加するだけで簡単に実現できます。あわせて、updateアクションがJSONタイプのリクエストを処理できるようにします。ここでは空のオブジェクトを返すだけですが。
なお、本番アプリでは、if/elsif/else条件ではなく、別の戦略で操作のサニタイズを行うことをお勧めします。このケースでは、MediatorやProxyパターンを採用するのが良いでしょう。
RubyにおけるTrixの限界
ここまでの説明は一見完璧に思えますが、重要な詳細を1つ省略していました。基盤となるデータベースモデルは問題なく永続化できていますが、Trixの内部的なシャドウ表現とは同期されていません。そのため、テーブルからフォーカスを外すと、以前保存されていた状態に戻ってしまうのです。

ページを更新すれば、追加した内容が表示されます。Trixのドキュメントが新しく初期化されるからです。
この問題の原因を突き止めたところ、選択範囲が変化したときにTrixが内部ドキュメントを同期する箇所にあることがわかりました。シャドウ要素から展開しているだけなのです。
turbo:submitイベントにフックして、テーブルからフォーカスが外れる瞬間だけ同期を防止しようと試みましたが、思いついた解決策はどれも非常に複雑で、内部APIへの依存度が高すぎました。
おそらく最もTurboらしい対処法は、フォーム全体をeager-loadedなTurbo Frameでラップし、Trixのコンテンツが変化するたびにリロードするよう指示することだと思います。
次のようなコードでうまくいくはずです。
srcから読み込むTurbo Frameでフォームを囲む場合は次のようになります。
ただし、このアプローチは、すでに永続化されているベースレコードでのみ機能します。
Trixに関する最終的な注意点
今回構築した概念実証は、テーブルをJSONにシリアライズしたりJavaScriptイベントを監視したりする複雑さを排除するため、サーバーレンダリングされたHTMLを活用しています。任意のActionText環境に移植可能で、gemとして簡単に切り出すこともできるでしょう。
ただし、いくつかの欠点もあります。最も明白なのは、Trixのドキュメントモデルとの再同期が必要になる点です。提案した回避策が機能する場面もあれば、採用できない場面もあるでしょう。TrixがTurbo互換のインターフェースを備えるまでは、この問題を回避する術はありません。
2つ目の懸念は、Trixのundo機能が使えないことです(ただし、これはどのTrixアタッチメントにも共通する課題です)。同様に、内部APIに手を加えるよりも、上流の変更を待つのが賢明でしょう。
まとめ
この記事では、まずActionTextアタッチメントの基本を簡単に確認しました。次にActionTextモデルにテーブルを追加し、Turbo Framesを使って操作できるようにしました。最後に、Trixを使用する際のいくつかの制限についても触れました。
CoffeeScriptから最新のプレーンJavaScriptへの移行を特徴とするTrix v2が進行中であることを考えると、Turbo互換性に取り組む絶好のタイミングです。現時点では、そのようなラッパーがどのような形になるかは私の能力の範囲を超えていますが、確かに大きなチャンスのように見えます。
ハッピーコーディング!
P.S. Ruby Magicの記事を公開後すぐに読みたい方は、Ruby Magicニュースレターを購読して、記事を見逃さないようにしましょう!
-
Ruby 4.0の主要機能とリリースハイライト、アップグレードガイド
Ruby 4.0は、2025年12月25日、Ruby誕生30周年を記念してリリースされた大型バージョンアップです。破壊的な変更を伴うものではなく、30年間のコミュニティの歴史を祝う意味合いが込められています。 実はRubyはセマンティックバージョニング(semantic versioning)に厳密には従っていないことを知って驚きました。 代わりに、Rubyの生みの親であるMatz(まつもとゆきひろ氏)は、変更内容が彼を「感動させた」ときにメジャーバージョンを引き上げるのです。このバージョンはRubyの30周年を記念するとともに、言語をさらに拡張する機能を多数導入しています。 Ruby 4へ
-
【Rails】Flashメッセージの使い方を徹底解説!alertとnoticeの違いからflash.nowまで
Flashメッセージとは? Flashメッセージとは、Railsアプリケーションのユーザーに対して、自分の操作によって何が起こったのかを伝えるための仕組みです。 メッセージの例: 「パスワードが正しく変更されました」(確認メッセージ) 「ユーザーが見つかりません」(エラーメッセージ) Flashメッセージはコントローラーで設定し、ビューで表示します。ユーザーはそのメッセージを受け取って、次のアクションにつなげられます。 それでは、具体的な使い方を見ていきましょう! Flashメッセージの基本的な使い方 通知メッセージは、flashヘルパーメソッドを使って扱います。 flashオブジェクトは