Hotwire勉強会ログ (1) Turbo

# はじめに

Rails 7から標準となったHotwireについて勉強会をしました。前半はTurboで後半がStimulusです。場所はSENQ六本木のスカイテラスで、晴れていたのですが少し寒かったかもしれません。

https://www.senq-web.jp/office/roppongi

Railsで有名な万葉さんもHotwireをやっていくそうです。

https://note.com/everyleaf/n/ncc0b77578c58

私の場合は最近のRailsのJavaScriptの機能は使っていなくて、Turbolinksはオフにすることが多かったです。Rails 7ではHotwireが標準になってrails-ujsもフェードアウトしていきそうなので、ここは観念して勉強しようと思いました。

そしてForkwellさんのDHHへのインタービュー動画を見たことも影響しました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/wzBCGHmv-jY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

というわけで、まずはHotwireのTurboをやっていきます。

# Turboの概要

- Turbo Drive
    - ページ全体を更新
    - Turbolinksと同じようなもの
- Turbo Frames
    - ページの一部を更新
- Turbo Streams
    - 複数箇所の更新が可能
- Turbo Native
    - iOSの`WKWebView`やAndroidの`WebView`インスタンスを管理

以下の記事も参照してください。

https://note.com/everyleaf/n/n842f5020d43b

# Turbo Drive

- Turbolinksの後継
- 進行状況の表示
    - デフォルトは500ミリ秒
- フォームの送信イベントの監視やレンダリングの中止などをJavaScriptで操作できる
    - これを使うことは少なそうだけど必要なこともあるのでしょうか
- 特定のリンク先でTurboを無効
    - サードパーティのJavaScriptを使うときなど
    - これはTurbolinksでも使ったことがありました
- フォーム送信後のリダイレクト
    - `see_other`とかいろいろと注意するポイントがあるようです

https://qiita.com/jnchito/items/5c41a7031404c313da1f

- リンクをキャッシュにプリロードする
    - `<a href="/" data-turbo-preload>Home</a>`でキャッシュさせることができて使うことがあるかも

# Turbo Frames

- `<turbo-frame>`要素を変更する
    - 1回の操作で変更できるのは1つの`<turbo-frame>`のみ
    - フレームに対する操作は置換のみ
- `<turbo-frame>`には一意のIDが必要
- Eager-LoadingとLazy-Loadingがある
- 通常はフレームの操作はWebブラウザーの履歴には反映されないが、反映させることも可能

# Turbo Streams

- `<turbo-stream>`要素を変更する
    - 複数の`<turbo-stream>`を変更することができる
- 7つのアクション
    - append / prepend / before / after / replace / update / remove
    - これらのアクションで対応できない時はStimulusコントローラーを使用する
- WebSocketを利用してメールやチャットの受信のようにサーバーから起動することもできる

# 素振り

私がRailsでアプリを開発しはじめたときに読んでいた[Rails3レシピブック 190の技](https://www.sbcr.jp/product/4797363821/)には項目にタグが付けられていて、`素振り重要`というタグが印象的でした。きっとTurboも`素振り重要`だと思うので、素振りをしていきたいと思います。

Hotwireの入門にはすばらしい教材があります。

https://zenn.dev/shita1112/books/cat-hotwire-turbo

今回の勉強会では時間が限られているため、Scaffoldで生成されるコードに最小限の変更でTurboを体験できるようにしました。アプリの操作手順によっては正しく動作しないので、操作をリセットする場合は`http://localhost:3000/contents`にアクセスしてください。

```bash
% rails new sampleapp
% cd sampleapp
% rails g scaffold Content title:string body:text
% rake db:migrate
```

まずは適当にデータを３件ほど登録しておきます。

## フォームの入力で表示条件を変更

Turbo Framesを使ってリストを更新するように変更します。

```erb:app/views/contents/index.html.erb
<%= form_with url: contents_path, method: :get, data: { turbo_frame: "contents-list" } do |f| %>
  <%= f.number_field :limit %>
  <%= f.submit "submit" %>
<% end %>

<%= turbo_frame_tag "contents-list" do %>
  <% @contents.each do |content| %>
    <%= render content %>
    <p>
      <%= link_to "Show this content", content %>
    </p>
  <% end %>
<% end %>
```

```ruby:app/controller/contents_controller.rb
def index
  if params[:limit]
    @contents = Content.all.limit(params[:limit])
  else
    @contents = Content.all
  end
end
```

フォームに表示件数を入力して`submit`ボタンをクリックすると、指定した表示件数を上限としたコンテンツが表示されます。このとき`GET`メソッドを利用しているので通常はURLで引数が渡されますが、今回はTurbo Framesで処理しているためURLは変更されていません。

## 編集フォームをインラインで表示

それぞれのコンテンツを`turbo_frame_tag`で囲い、その中に編集リンクを追加します。

```erb:app/views/contents/index.html.erb
<%= turbo_frame_tag "contents-list" do %>
  <% @contents.each do |content| %>
    <%= turbo_frame_tag content do %>
      <%= render content %>
      <p>
        <%= link_to "Edit this content", edit_content_path(content) %>
        <%= link_to "Show this content", content %>
      </p>
    <% end %>
  <% end %>
<% end %>
```

`edit.html.erb`を`turbo_frame_tag`で囲います。`h1`タグは削除します。

```erb:app/views/contents/edit.html.erb
<%= turbo_frame_tag @content do %>
  <%= render "form", content: @content %>

  <br>

  <div>
    <%= link_to "Show this content", @content %> |
    <%= link_to "Back to contents", contents_path %>
  </div>
<% end %>
```

コントローラーで`update`のリダイレクト先を`contents_url`にします。

```ruby:app/controller/contents_controller.rb
def update
  respond_to do |format|
    if @content.update(content_params)
      format.html { redirect_to contents_url, notice: "Content was successfully updated." }
      format.json { render :show, status: :ok, location: @content }
    else
      format.html { render :edit, status: :unprocessable_entity }
      format.json { render json: @content.errors, status: :unprocessable_entity }
    end
  end
end
```

これでインラインの編集が可能になりました。

## フラッシュメッセージの表示

Turbo Streamsを使ってフラッシュメッセージを表示するようにします。

```ruby:app/controller/contents_controller.rb
def update
  if @content.update(content_params)
    flash.now.notice = "Content was successfully updated."
  else
    render :edit, status: :unprocessable_entity
  end
end
```

```erb:app/views/contents/update.turbo_stream.erb
<%= turbo_stream.replace @content %>
<%= turbo_stream.update "flash", partial: "flash" %>
```

```erb:app/views/contents/_flash.html.erb
<% if notice %>
  <div><%= notice %></div>
<% end %>
```

`index.html.erb`の上部にフラッシュメッセージを表示するようにします。

```erb:app/views/contents/index.html.erb
<div id="flash">
  <%= render "flash" %>
</div>
```

これでフラッシュメッセージが表示できましたが、コンテンツを更新すると`Edit this content`と`Show this content`のリンクが消えます。このリンクを維持する場合は`_content.html.erb`にリンクを移動します。

# おわりに

簡単な例ではありますが、HotwireのTurbo FramesとTurbo Streamsの動作を体験することができました。来年は既存のRailsアプリをRails 7に移行することがありそうで、Hotwireにも対応させていきたいと思います。