Fusic Advent Calendar 2025: Day2
turbo-frameを使って、ある要素を部分更新とか追加とか動的な変更をjavascriptを利用せずともサクッと更新できてとても便利ですね。
個人的にはビューやパーシャル、コントローラなど色んなファイル見なきゃいけなくて、若干の苦手意識があるんですが。。。
今回は、テーブルの部分更新にturbo-frameの利用でやや手こずったので記録として残しておきます。
概要
例えば、以下のようなtable構造を考えます。
<table>
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>User</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @rooms.each do |room| %>
<%= turbo_frame_tag dom_id(room) do %>
<%= render room %>
<% end %>
<% end %>
</tbody>
</table>
やりたいこととして、render roomのところで、turbo_frame_tagで囲われたtbodyの要素を更新し続けていくってことです。
自分が遭遇した事例としては、テーブルの中に編集ボタンがあり、編集画面へ遷移でなく、エクセルのようにその場での編集可能な形に仕上げたいといったことです。
初めての要望でもなかったので、割と需要あったりするのかもしれません。
| 部屋番号 | 契約者 | ↓ボタンを押すと、行が編集できるようになる仕様 |
|---|---|---|
| 001 | テスト01 | 編集 |
| 002 | テスト02 | 編集 |
| ... | ... | ... |
これを実現するために、table構造イカれてるけど、上記HTMLのコードを書けばいけると思ってましたが、ダメでした。
レンダリングされるHTML
先ほど書いたHTMLがどうレンダリングされるかですが、こう書いた人は以下の形で出ることを期待するかと思います。
<!-- 期待した出力 -->
<table>
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>User</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<turbo-frame id="room_1">
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</turbo-frame>
<turbo-frame id="room_2">
...
</tbody>
</table>
htmlの構造的な違反があるとはいえ、とりあえず動くだろうと期待していました。
が、実際には以下のHTMLが出力されます。
<!-- 実際の出力 -->
<turbo-frame id="room_1"></turbo-frame>
<turbo-frame id="room_2"></turbo-frame>
...
<table>
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>User</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody>
</table>
turbo-frameタグは上に押し上げられ、正しいtable構造が出力されるようにレンダリングされます。
例えば部分更新用のコードを以下のよう書いていると全く意図しない挙動となります。
<%= turbo_frame_tag dom_id(@room) do # room.id = 1 %>
<tr>
<td>...</td>
<td>...</td>
<td>
<!-- ビュー更新部分 -->
<input type="text" value="...">
</td>
<td>...</td>
</tr>
<% end %>
↓ これが<turbo-frame id="room_1"></turbo-frame>に差し込まれ、
<turbo-frame id="room_1">
<!-- ここに差し込まれる -->
<%= turbo_frame_tag dom_id(@room) do # room.id = 1 %>
<tr>
<td>...</td>
<td>...</td>
<td>
<!-- ビュー更新部分 -->
<input type="text" value="..."></input>
</td>
<td>...</td>
</tr>
<% end %>
</turbo-frame>
<turbo-frame id="room_2"></turbo-frame>
...
<table>
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>User</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody>
</table>
のような感じになってしまいます。
解決方法
一言で言えば、turbo-streamのreplaceを使いましょうで解決しました。
コードだけ先に書くと以下です。
<%= turbo_frame_tag "inline-editable-frame" %>
<table>
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>User</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @rooms.each do |room| %>
<%= render room %>
<% end %>
</tbody>
</table>
<!-- _room.html.erb -->
<tr id="<%= dom_id(room) %>">
<td>...
</tr>
<!-- ---------------- -->
<!-- 更新用ビュー -->
<%= turbo_frame_tag "inline-editable-frame" do %>
<turbo-stream action="replace" target="<%= dom_id(@room) %>">
<template>
<tr id="<%= dom_id(@room) %>">
<td>...</td>
<td>...</td>
<td>
<input type="text" value="..."></input>
</td>
<td>...</td>
</tr>
</template>
</turbo-stream>
<% end %>
<% # もしくは、turbo_stream.replaceでturbo_streamを直接返すか。%>
<!-- ---------------- -->
上記のようなコードを実装することで以下の流れを得ます。
- ページ上に「空の
turbo_frame_tag "inline-editable-frame"」を置く - 行の編集リンクからその frame に対してリクエストを飛ばす
- レスポンスでは、その frame を中継地点にして
<turbo-stream action="replace">を返す -
target="<%= dom_id(@room) %>"の<tr>を置き換える
冷静になって考えれば、まあ確かにそうかといった感じです。
turbo_frame_tagは実質的には、運搬用の箱のような役割を演じます。
てか、あんまりこういう使い方しない気もするので、普通思いつかんだろって感じではある。
hotwired/turbo issue48
同じ問題に当たったことある人は見てるかもしれません。
てか、探した感じ、ここしか解決策なかったので見てると思います。
参考 > https://github.com/hotwired/turbo/issues/48
この中を見る感じ、Rails7以降では、そもそもscaffoldにtableを使うことを削除されるから、他の方法使うよう促されているような感じです。
しばらくRails離れている期間あったので、知らんかったですが。。。
そんなこと言っても、あるものは使えるんだし使うもんだし。
てか元々table使われていたら、そうも言ってられないと思います。
解決策書いてくれてるところがこれ。
https://github.com/hotwired/turbo/issues/48#issuecomment-2103585520
最近は、「LLM > 未解決ならLLMが出したキーワードでweb検索 > 何も出なければ自分でキーワード探す > githubとかブログとかで探す」の手順を踏んでいますが、LLMの万能感すごくて頼りすぎちゃうことも多く、こういう調査能力落ちてる気がする。。。
特に今回に限って、「LLM > 未解決ならLLMが出したキーワードでweb検索」この手順確実に無駄だったし。
前はすぐにgithubやドキュメント漁りしていた気がしますが。
この辺の嗅覚も必要だなあと思います。