hotwireの学習①

# はじめに
この記事は、hotwireの学習についての備忘録です。

https://qiita.com/youfuku/items/782b0a7f974e2a53d89e

前回の記事の続き(importmap + bootstrap)です。
また、記事が長くなると思われるので、数回に分割する予定です
今回は、主に「Turbo Frames」に絞った内容です。

# hotwireとは
他の記事に詳細が書かれてるので、この場ではさらっとした説明です。
HotwireはRails7からRailsのフロントエンドのデフォルトとなった技術です。
また、複数の機能を統合した相称の名前となります。

- Hotwire
    - Turbo
        - Turbo Drive  　　　・・・画面描写を早くする
        - Turbo Frames　　・・・部分差し替えを行う(主にgetで利用)
        - Turbo Streams　　・・・部分差し替えを行う(主にget以外で利用)
    - Stimulus　　　　　　・・・rails専用のJS

https://nekorails.hatenablog.com/entry/2022/05/16/170434


# todo作成

基本画面を作成
```shell:コマンド
docker compose run --rm app bundle exec rails generate scaffold todo content:string complete:boolean
```

テストデータを登録するseedファイルを準備します
```ruby:app/db/seeds.rb
Todo.create(
  [
    {content: "柳田 政年" , complete: 0},
    {content: "佐野 護" , complete: 0},
    {content: "小松 純典" , complete: 0},
    {content: "堀川 陽子" , complete: 0},
    {content: "堀川 武士" , complete: 0},
    {content: "松原 竹義" , complete: 0},
    {content: "山崎 詔勝" , complete: 0},
    {content: "大内 武俊" , complete: 0},
    {content: "藤村 広重" , complete: 0},
    {content: "白川 芳尚" , complete: 0},
    {content: "大塚 美砂" , complete: 0},
    {content: "茂木 幸郎" , complete: 0},
    {content: "辻 和広" , complete: 0},
    {content: "東 茂俊" , complete: 0},
    {content: "豊田 世弥" , complete: 0},
    {content: "笠原 永寿" , complete: 0},
    {content: "浅井 陽一郎" , complete: 0},
    {content: "吉沢 祐子" , complete: 0},
    {content: "中野 貢太郎" , complete: 0},
    {content: "奥野 貢太郎" , complete: 0},
    {content: "竹内 芳伸" , complete: 0},
    {content: "花田 十四夫" , complete: 0},
    {content: "西川 重文" , complete: 0},
    {content: "後藤 一樹" , complete: 0},
    {content: "古田 早葉子" , complete: 0},
    {content: "坂井 裕仁" , complete: 0},
    {content: "中野 昌光" , complete: 0},
    {content: "堀江 茂也" , complete: 0},
    {content: "小野 理江" , complete: 0},
    {content: "徳田 真里" , complete: 0},
    {content: "古谷 督彦" , complete: 0},
    {content: "田代 健生" , complete: 0},
    {content: "古谷 常一" , complete: 0},
    {content: "藤村 克三" , complete: 0},
    {content: "横山 琢司" , complete: 0},
    {content: "浅野 元信" , complete: 0},
    {content: "古谷 政年" , complete: 0},
    {content: "小田 三佐男" , complete: 0},
    {content: "村瀬 尭道" , complete: 0},
    {content: "武井 恵治" , complete: 0},
    {content: "土屋 孝次" , complete: 0},
    {content: "大場 令" , complete: 0},
    {content: "久保 友良" , complete: 0},
    {content: "大平 美津枝" , complete: 0},
    {content: "山岸 時司" , complete: 0},
    {content: "下田 美里" , complete: 0},
    {content: "岡 敬志" , complete: 0},
    {content: "森田 道和" , complete: 0},
    {content: "白川 恵" , complete: 0},
    {content: "松本 俊憲" , complete: 0},
    {content: "秋田 秋代" , complete: 0},
    {content: "安井 勝英" , complete: 0},
    {content: "藤川 敬生" , complete: 0},
    {content: "谷川 元三" , complete: 0},
    {content: "山本 興亜" , complete: 0},
    {content: "小松 良司" , complete: 0},
    {content: "竹下 勤" , complete: 0},
    {content: "新田 酉蔵" , complete: 0},
    {content: "竹本 幸四郎" , complete: 0},
    {content: "野崎 慶太郎" , complete: 0},
    {content: "古谷 朋子" , complete: 0},
    {content: "大森 裕子" , complete: 0},
    {content: "荻野 生三" , complete: 0},
    {content: "丹羽 重三郎" , complete: 0},
    {content: "坂田 安民" , complete: 0},
    {content: "堀 十四夫" , complete: 0},
    {content: "奥山 義将" , complete: 0},
    {content: "小出 成男" , complete: 0},
    {content: "馬場 育雄" , complete: 0},
    {content: "高瀬 夕子" , complete: 0},
    {content: "福原 和久" , complete: 0},
    {content: "大沢 允彦" , complete: 0},
    {content: "本間 多栄子" , complete: 0},
    {content: "細川 敬" , complete: 0},
    {content: "植田 麻紀" , complete: 0},
    {content: "千田 三重子" , complete: 0},
    {content: "緒方 裕恵" , complete: 0},
    {content: "福岡 美緒" , complete: 0},
    {content: "野田 常次" , complete: 0},
    {content: "藤川 真樹" , complete: 0},
    {content: "横井 紀子" , complete: 0},
    {content: "堀内 美里" , complete: 0},
    {content: "本間 十四夫" , complete: 0},
    {content: "松山 勝行" , complete: 0},
    {content: "福永 優紀" , complete: 0},
    {content: "関根 洋昌" , complete: 0},
    {content: "富田 忠之" , complete: 0},
    {content: "平山 勝司" , complete: 0},
    {content: "田上 恵治" , complete: 0},
    {content: "増田 雅則" , complete: 0},
    {content: "古谷 俊郎" , complete: 0},
    {content: "安部 善二" , complete: 0},
    {content: "土田 十四夫" , complete: 0},
    {content: "池上 玲子" , complete: 0},
    {content: "福井 純隆" , complete: 0},
    {content: "大石 梨華" , complete: 0},
    {content: "尾崎 聡" , complete: 0},
    {content: "島田 利克" , complete: 0},
    {content: "堀内 唯" , complete: 0},
    {content: "福本 美津枝" , complete: 0},
  ]
)
```

DB構築及びテストデータを登録
```
docker compose run --rm app bundle exec rails db:drop db:create db:migrate db:seed
```

全体的に淵ギリギリに表示されるのは微妙なので、少しマージンを設定しときます
```diff:app/app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>App</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>
  
-  <body>
+  <body class="px-4">
    <%= yield %>
  </body>
</html>

```

トップページに、作成したtodoへのリンクを配置しておきます
```diff:app/app/views/top/index.html.erb
<h1>Top#index</h1>
<p>Find me in app/views/top/index.html.erb</p>

<button type="button" class="btn btn-primary">Primary</button>
<button type="button" class="btn btn-secondary">Secondary</button>
<button type="button" class="btn btn-success">Success</button>
<button type="button" class="btn btn-danger">Danger</button>
<button type="button" class="btn btn-warning">Warning</button>
<button type="button" class="btn btn-info">Info</button>
<button type="button" class="btn btn-dark">Dark</button>

<button type="button" class="btn btn-link">Link</button>


<div class="btn-group">
  <button type="button" class="btn btn-danger dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
    Action
  </button>
  <ul class="dropdown-menu">
    <li><a class="dropdown-item" href="#">Action</a></li>
    <li><a class="dropdown-item" href="#">Another action</a></li>
    <li><a class="dropdown-item" href="#">Something else here</a></li>
    <li><hr class="dropdown-divider"></li>
    <li><a class="dropdown-item" href="#">Separated link</a></li>
  </ul>
</div>

+ <hr>
+ <%= link_to 'todo', todos_path, class: 'btn btn-primary' %>
```

この時点でのトップページはこんな感じになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/d72567d9-aece-798f-3d53-e6920f8ceb35.png)

## indexの修正

画面は、昔のテーブル形式にしておきます
```erb:app/app/views/todos/index.html.erb
<h1>Todos</h1>
<%= link_to "New todo", new_todo_path %>
<table class="table table-bordered mt-2">
  <thead>
    <td>ID</td>
    <td>Content</td>
    <td></td>
  </thead>
  <tbody>
    <% @todos.each do |todo| %>
      <%= render todo %>
    <% end %>
  </tbody>
</table>
```

```erb:app/app/views/todos/_todo.html.erb
<tr>
  <td>
    <%= todo.id %>
  </td>
  <td>
    <%= todo.content %>
  </td>
  <td>
    <div class="d-flex justify-content-end">
      <%= link_to "Show", todo, class: "btn btn-sm btn-outline-primary me-2" %>
      <%= link_to "edit", edit_todo_path(todo), class: "btn btn-sm btn-outline-primary me-2" %>
      <%= button_to "Destroy", todo, method: :delete, data: { turbo_confirm: '本当に削除していいですか？' } , class: "btn btn-sm btn-outline-danger" %>
    </div>
  </td>
</tr>
```

### 画面確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/d06142f8-327b-b6d6-993e-991370f78efd.png)

## ページネートを追加

```ruby:app/Gemfile
gem "kaminari"
gem 'bootstrap4-kaminari-views'
```

```ruby:コマンド
docker compose build --no-cache
docker compose run --rm app bundle install
```

```diff:app/app/controllers/todos_controller.rb
  # GET /todos or /todos.json
  def index
-   @todos = Todo.all
+   @todos = Todo.all.page(params[:page]).per(10)
  end
```

```diff:app/app/views/todos/index.html.erb
<h1>Todos</h1>
<%= link_to "New todo", new_todo_path %>
<table class="table table-bordered mt-2">
  <thead>
    <td>ID</td>
    <td>Content</td>
    <td></td>
  </thead>
  <tbody>
    <% @todos.each do |todo| %>
      <%= render todo %>
    <% end %>
  </tbody>
</table>
+ <%= paginate @todos, theme: 'twitter-bootstrap-4'%>
```

### 画面確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/6c9f55a5-d232-b8cb-2a17-51c4000b0c8c.png)
見た目はこんな感じになります

![1709014517.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/a59510fb-7de9-e479-330c-69e544309d2e.gif)
動きとしても従来通りのURLにページ番号が乗ってる状態です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/69e107a2-563e-8287-f7ef-5f5485f6f101.png)
コンソールでは、こんな感じに表示されており、平均速度35～45msという感じでした

## コレクションキャッシュを使ってみる
```diff:app/app/views/todos/index.html.erb
<h1>Todos</h1>
<%= link_to "New todo", new_todo_path %>
<table class="table table-bordered mt-2">
  <thead>
    <td>ID</td>
    <td>Content</td>
    <td></td>
  </thead>
  <tbody>
-     <% @todos.each do |todo| %>
-       <%= render todo %>
-     <% end %>
+     <%= render partial: 'todo', collection: @todos, cached: true %>
  </tbody>
</table>
<%= paginate @todos, theme: 'twitter-bootstrap-4'%>
```

### 画面確認
![1709014618.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/e831f3e0-f021-d382-40fb-52133e6366a1.gif)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/4c8098fc-3f4d-9195-1286-e1256e53ac38.png)
レンダリングが少し早くなって、平均速度25～35msという感じでした

## Turbo Frames

```diff:app/app/views/todos/index.html.erb
<h1>Todos</h1>
<%= link_to "New todo", new_todo_path %>
+ <%= turbo_frame_tag "todos-table" do %>
  <table class="table table-bordered mt-2">
    <thead>
      <td>ID</td>
      <td>Content</td>
      <td></td>
    </thead>
    <tbody>
      <%= render partial: 'todo', collection: @todos, cached: true %>
    </tbody>
  </table>
  <%= paginate @todos, theme: 'twitter-bootstrap-4'%>
+ <% end %>
```

### 画面確認
![1709015008.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/51ac8baf-5797-0744-8dec-22bb5ec284d4.gif)
URLを確認するとURLが変わらずSPA化できてます！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/bd5b1ba9-d10d-c6db-e39d-32b92613d528.png)
応答速度を確認したところ、15～25msという感じでした


# 最後に
今回はここまでにします。
Turbo Framesを使うことで、簡単にSPA化できてることに驚いています。
次回は、引き続きTODOの更新関係を手掛け、Turbo Streamsについて学んでいきます