Rails: Hotwire: Turbo StreamsなScaffoldはこんな感じかな?

外観

目的
メッセージのScaffoldを作成した後、できるだけ生成したコードを手直しせずに、Turbo Streamsを用いて一覧画面からメッセージの作成、編集と削除を行えるようにしたいと思います。尚、今回はWebSocketを用いた更新は扱いません。

ということで今回はTurbo Streamsを前提としたScaffoldはこんな感じになるかなとコードを追加修正してみます。完成後の画面は次のようになります。

画像8

環境
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降順で一覧表示する。追加した項目を上に表示するため。
追加: 画面上部に作成フォームを追加する。
削除: 画面下部の作成フォームへのリンクを削除する。

画像6

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
先にメッセージの作成に失敗した場合から実装していきます。が、まとめて記載した方がわかりやすい箇所は成功時のコードも並べて実装しています。

画像2


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
次にメッセージの作成に成功した場合を実装します。

画像3

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
追加: 編集フォームを追加する。

スクリーンショット 2021-01-10 18.53.32

編集リンクは実装済みです。
コントローラーの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
編集フォームにキャンセルボタンを追加します。

追加: 編集キャンセルのリンクを追加する。

画像5

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;
}

画像7

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のスタイルガイドも気になるところです。今後の展開が楽しみです。

以上です。


この記事が気に入ったらサポートをしてみませんか?