Rails: Hotwire: Turbo StreamsなScaffoldはこんな感じかな?
外観
目的
メッセージのScaffoldを作成した後、できるだけ生成したコードを手直しせずに、Turbo Streamsを用いて一覧画面からメッセージの作成、編集と削除を行えるようにしたいと思います。尚、今回はWebSocketを用いた更新は扱いません。
ということで今回はTurbo Streamsを前提としたScaffoldはこんな感じになるかなとコードを追加修正してみます。完成後の画面は次のようになります。
環境
Ruby 2.7.2
Rails 6.1.1
Turbo 7.0.0-beta.2, Turbo 7.0.0-beta.3
※この実装では Turbo 7.0.0-beta.4 で編集とキャンセルが動かなくなりました。一覧に<table>を使わないで良いのならturbo_frame_tagで代用できます。下記のマージでscaffoldが変更され<table>が無くなりましたので、Rails 7ではturbo_frame_tagを使用することができそうです。
参照
CRUDの準備
タイトルとコンテンツを持つメッセージのCRUDをScaffoldで用意します。
今回のアプリはJavaScriptをスキップして作成しjbuilder GEMを用いません。
rails new try_hw_scaffold --skip-javascript
cd try_hw_scaffold
tmux
bin/bundle remove jbuilder
bin/bundle add hotwire-rails
bin/rails hotwire:install
bin/bundle install
bin/rails g scaffold message title content:text
bin/rails db:migrate
Create(生成)とRead(読み取り)
new
一覧画面の上部に作成フォームを追加します。
変更: ID降順で一覧表示する。追加した項目を上に表示するため。
追加: 画面上部に作成フォームを追加する。
削除: 画面下部の作成フォームへのリンクを削除する。
app/controllers/messages_controller.rb
# GET /messages
def index
@messages = Message.all.order(id: :desc)
@message = Message.new
end
app/views/messages/index.html.erb
<p id="notice"></p>
<h1>Message</h1>
<%= render 'form', message: @message %>
<h2>Messages</h2>
# ...
-
-<br>
-
-<%= link_to 'New Message', new_message_path %>
app/assets/stylesheets/scaffolds.scss
通知表示の際に縦スクロールの位置が変わらないように画面上部で固定します。
#notice {
z-index: 1;
position: fixed;
top: 0px;
color: green;
background-color: #eee;
}
create failed
先にメッセージの作成に失敗した場合から実装していきます。が、まとめて記載した方がわかりやすい箇所は成功時のコードも並べて実装しています。
app/models/message.rb
保存の失敗を試しやすくするために、モデルにバリデーションを追加します。
class Message < ApplicationRecord
validates :title, presence: true
app/controllers/messages_controller.rb
保存の生成/失敗時のコードを追加し@noticeに処理結果に応じた文言を設定します。
@new_messageは後述する作成フォームの置き換えに用います。保存に成功した場合は新規のフォームを表示するためにMessage.newを設定し、保存に失敗した場合はエラーを表示するために@messageを設定します。
# POST /messages
def create
@message = Message.new(message_params)
if @message.save
@new_message = Message.new
@notice = 'Message was successfully created.'
else
@new_message = @message
@notice = ''
end
end
app/views/messages/create.turbo_stream.erb
ファイルを追加します。以下、*.turbo_stream.erbファイルは追加になります。
<% if @message.valid? %>のブロックは保存に成功した場合のコードです。保存に成功したらturbo_stream.prependを用いてメッセージ一覧の先頭行にメッセージを追加します。
turbo_stream.replaceを用いて、@new_messageで作成フォームを置き換えます。
<% if @message.valid? %>
<%= turbo_stream.prepend(
:messages,
partial: 'message',
locals: { message: @message }
) %>
<% end %>
<%= turbo_stream.replace(
'new_message',
partial: 'form',
locals: { message: @new_message }
) %>
<%= render 'notice' %>
app/views/messages/_notice.turbo_stream.erb
turbo_stream.updateで文言を更新します。
<%= turbo_stream.update(
:notice,
"#{@notice}"
) %>
app/views/messages/_form.html.erb
作成フォームは編集時と共通する部分が多いためメッセージの編集と共有します。それぞれのメッセージを特定できるようにするため、id: dom_id(message)を追加します。
<%= form_with(model: message, id: dom_id(message)) do |form| %>
create succeeded
次にメッセージの作成に成功した場合を実装します。
app/views/messages/index.html.erb
テーブル行の先頭にメッセージを追加したり置き換えるので、メッセージ行を部分テンプレートに切り出します。
<p id="notice"><%= notice %></p>
<h1>Message</h1>
<%= render 'form', message: @message %>
<h2>Messages</h2>
<table>
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th colspan="2"></th>
</tr>
</thead>
<tbody id="messages">
<%= render partial: 'message', collection: @messages %>
</tbody>
</table>
app/views/messages/_message.html.erb
切り出した際に、使用しないメッセージ表示のリンクを削除し、メッセージ削除リンクはbutton_toに置き換えます。rails-ujsを用いないためlink_toでPOSTできないためです。また現状では確認ダイアログは表示できませんので、前回行ったようにStimulusでダイアログを表示すると良いかもしれません。
<tr id="<%= dom_id message %>">
<td><%= message.title %></td>
<td><%= message.content %></td>
<td><%= link_to 'Edit', edit_message_path(message) %></td>
<td><%= button_to 'Destroy', message, method: :delete %></td>
</tr>
Delete(削除)とRead(読み取り)
destroy succeeded
削除: 一覧から表示リンクを削除する。
変更: 一覧の削除リンクをbutton_toに変更する。
<td><%= button_to 'Destroy', message, method: :delete %></td>
# DELETE /messages/1
def destroy
@message.destroy
@notice = 'Message was successfully destroyed.'
end
app/views/messages/destroy.turbo_stream.erb
<%= turbo_stream.remove(@message) %>
<%= render 'notice' %>
Update(更新)とRead(読み取り)
Editリンクをクリックしたメッセージ行を編集フォームに置き換えます。
update succeeded/failed
追加: 編集フォームを追加する。
編集リンクは実装済みです。
コントローラーのeditで@noticeの文言を空に設定して、編集フォームを表示します。
<td><%= link_to 'Edit', edit_message_path(message) %></td>
# GET /messages/1/edit
def edit
@notice = ''
end
app/views/messages/edit.turbo_stream.erb
turbo_stream.replaceでメッセージ行を編集フォームに置き換えます。メッセージの部分テンプレートをrenderする際にはcollectionの時とは異なりformatsの自動変換は行われませんのでHTMLを設定します。
<%= turbo_stream.replace @message do %>
<tr id="<%= dom_id @message %>">
<td colspan="4">
<%= render(
partial: 'form',
locals: { message: @message },
formats: :html
) %>
</td>
</tr>
<% end %>
<%= render 'notice' %>
# PATCH/PUT /messages/1
def update
if @message.update(message_params)
@notice = 'Message was successfully updated.'
else
render :edit
end
end
app/views/messages/update.turbo_stream.erb
<%= turbo_stream.replace(
@message,
partial: 'message',
locals: { message: @message }
) %>
<%= render 'notice' %>
update canceled
編集フォームにキャンセルボタンを追加します。
追加: 編集キャンセルのリンクを追加する。
app/views/messages/_form.html.erb
#...
<% if message.persisted? %>
<div class="actions">
<%= link_to 'Cancel', message %>
</div>
<% end %>
app/views/messages/show.turbo_stream.erb
<%= turbo_stream.replace @message %>
フォームの着色
機能的には不必要ですが、フォームを着色して識別しやすくしてみます。
app/views/messages/_form.html.erb
messmessage_formクラスで全体を囲みます。
<div class="message_form">
<%= form_with(model: message, id: dom_id(message)) do |form| %>
...
<% end %>
</div>
app/assets/stylesheets/scaffolds.scss
.message_form {
background-color: #ecf9fd;
}
Heroku
ついでながらHerokuで公開した際に少し手間取ったので作業メモを残します。
現状ではjavascripts/librariesディレクトリの追加が必要でした。
git checkout -b pg-heroku
#bin/bundle update rails # Rail 6.1.1
bin/rails db:system:change --to=postgresql
bin/bundle install
mkdir app/assets/javascripts/libraries # Turbo 7.0.0-beta.3では不要
touch app/assets/javascripts/libraries/.gitkeep
git add .
git commit -m pg-heroku
#bin/rails db:setup
heroku create APP_NAME
heroku addons:create heroku-postgresql
git push heroku pg-heroku:master
heroku run rake db:migrate
#heroku run rake db:seed
heroku open
heroku logs --tail
最後に
これだけで画面の部分的な追加、置換、削除ができました。従来もjs.erbなどで同じようなことができましたが、典型的な手続きをHotwireが引き受けてくれるので実装がとても楽になりました。実装者間での不要なバラつきがなくなるのも良さそうですね。
世の中には様々なユースケースとそれを実現するユーザインタフェースがありますので、今後はそういったものをフィードバックしてフレームワークを洗練していく必要があります。バージョン番号から予想するとリリースはRails 7を目処にしているのかも。今回わたしは野生の勘で実装したためDHH氏がツイートしていたHotwireのスタイルガイドも気になるところです。今後の展開が楽しみです。
以上です。
この記事が気に入ったらサポートをしてみませんか?