hotwireの学習②

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

https://qiita.com/youfuku/items/e608683821f03d9a4cfe

前回の記事の続きです。
今回は、主に「Turbo Streams」に絞った内容です。


## 前回からの修正
情報が少ないので、showを使う場面あるっけ？と思いまして、リンクを削除します
```diff: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/d9457612-2a8a-6b75-b6f3-c0eba10fc9cf.png)


## 変更画面
```diff:app/app/views/todos/_todo.html.erb
<tr id=<%= dom_id(todo) %> >
  <td>
    <%= todo.id %>
  </td>
  <td>
    <%= todo.content %>
  </td>
  <td>
    <div class="d-flex justify-content-end">
-      <%= link_to "edit", edit_todo_path(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" , method: :get, data: { turbo_stream: true } %>
      <%= button_to "Destroy", todo, method: :delete, data: { turbo_confirm: '本当に削除していいですか？' } , class: "btn btn-sm btn-outline-danger" %>
    </div>
  </td>
</tr>
```
本当はTurbo Framesで置き換えたい気持ちもあったのですが、editのリンクを無理やりTurbo Streams化します

### 確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/5c956949-10cc-5083-53e3-0ea8356c38cf.png)
ボタンを押すとコンソールログに、Turbo Streams化されてることが表示されてるのが分かります。

```erb:app/app/views/todos/_edit_form.html.erb
<tr id="<%= dom_id(todo) %>" >
  <td>
    <%= todo.id %>
  </td>
  <td>
    <%= form_with(model: todo) do |form| %>
      <div class="input-group">
        <%= form.text_field :content , class: "form-control" %>
        <%= form.submit '更新' , class: "btn btn-outline-secondary" %>
      </div>
    <% end %>
  </td>
  <td>
  </td>
</tr>
```

```erb:app/app/views/todos/edit.turbo_stream.erb
<%= turbo_stream.replace dom_id(@todo) do %>
  <%= render partial: 'todos/edit_form', locals: { todo: @todo } %>
<% end %>
```

### 確認
![1709020036.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/48b7e590-b5e7-0b9d-0fd6-dba972221457.gif)
Turbo Streamsによって置換がされてるのが分かります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/cd79f5a6-19a7-13c3-5f42-71e2cdb94217.png)

コンソールを確認するとedit.turbo_stream.erbが動いたあと、_edit_form.html.erbを描写してるのが分かります。

## 元の画面に戻す(showを利用)
変更画面を最初の表の状態に戻す処理を作成します
```diff:app/app/views/todos/_edit_form.html.erb
<tr id=<%= dom_id(todo) %> >
  <td>
    <%= todo.id %>
  </td>
  <td>
    <%= form_with(model: todo) do |form| %>
      <div class="input-group">
        <%= form.text_field :content , class:"form-control" %>
        <%= form.submit '更新' , class:"btn btn-outline-secondary" %>
      </div>
    <% end %>
  </td>
  <td>
+    <div class="d-flex justify-content-end">
+      <%= link_to "戻る", todo_path(todo), class: "btn btn-sm btn-outline-primary me-2" , method: :get, data: { turbo_stream: true } %>
+    </div>
  </td>
</tr>
```


```erb:app/app/views/todos/show.turbo_stream.erb
<%= turbo_stream.replace dom_id(@todo) do %>
  <%= render partial: 'todos/todo', locals: { todo: @todo } %>
<% end %>
```

### 確認
![1709021020.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/6b5c27b0-c235-8807-02b0-80f3ddcb9ff4.gif)
戻るボタンを押すことで、元の状態に戻せることが確認できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/ea1bf57f-9c4f-e973-a22c-5f8a81767e3a.png)
コンソールログでも、showのturbo_streamが動いてるのが分かります

## 更新処理
```diff:app/app/controllers/todos_controller.rb
  def update
-    respond_to do |format|
-      if @todo.update(todo_params)
-        format.html { redirect_to todo_url(@todo), notice: "Todo was successfully updated." }
-        format.json { render :show, status: :ok, location: @todo }
-      else
-        format.html { render :edit, status: :unprocessable_entity }
-        format.json { render json: @todo.errors, status: :unprocessable_entity }
-      end
-    end

+    unless @todo.update(todo_params)
+      render :edit, status: :unprocessable_entity
+    end
  end
```


```erb:app/app/views/todos/update.turbo_stream.erb
<%= turbo_stream.replace dom_id(@todo) do %>
  <%= render partial: 'todos/todo', locals: { todo: @todo } %>
<% end %>
```

### 確認
![1709021535.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/d2a709b0-9acc-421a-ea4e-f842a1025697.gif)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/4d925704-7ed9-d9a6-ef75-3d310a64f6b5.png)
コンソールログでも、updateのturbo_streamが動いてるのが分かります

## 削除処理
```diff:app/app/controllers/todos_controller.rb
  def destroy
    @todo.destroy!

-    respond_to do |format|
-      format.html { redirect_to todos_url, notice: "Todo was successfully destroyed." }
-      format.json { head :no_content }
-    end
  end
```


```erb:app/app/views/todos/destroy.turbo_stream.erb
<%= turbo_stream.remove dom_id(@todo) do %>
<% end %>
```

### 確認
![l_6814799_112_8b4e2db53eddd518912d6d7fdf5fa20a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/170ee577-5da5-4086-18e8-6cd2bb76d364.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/71988cf1-74c3-c5c5-31a9-4364ec44b0c5.png)
コンソールログで、destroyのturbo_streamが動いてるのが分かります

## 新規登録画面
```erb:app/app/views/todos/_form.html.erb
<%= form_with(model: todo) do |form| %>
  <div class="input-group">
    <%= form.text_field :content , class:"form-control" %>
    <%= form.submit '登録' , class:"btn btn-outline-secondary" %>
  </div>
<% end %>
```

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


```diff:app/app/controllers/todos_controller.rb
  def create
    @todo = Todo.new(todo_params)

-     respond_to do |format|
-       if @todo.save
-         format.html { redirect_to todo_url(@todo), notice: "Todo was successfully created." }
-         format.json { render :show, status: :created, location: @todo }
-       else
-         format.html { render :new, status: :unprocessable_entity }
-         format.json { render json: @todo.errors, status: :unprocessable_entity }
-       end
-     end
+     @todo.save
  end
```


```erb:app/app/views/todos/create.turbo_stream.erb
<%= turbo_stream.append  "todos_tbody" do %>
  <%= render partial: 'todos/todo', locals: { todo: @todo } %>
<% end %>
```

### 確認
![1709022873.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/2c321f08-d99d-7bd8-c65c-63674709c3b8.gif)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/359bd760-5fe3-49db-6dd0-7959597e188b.png)
コンソールログで、createのturbo_streamが動いてるのが分かります

# 新規登録フォームの修正
登録後、新規登録フォームの値がそのままだったため、修正
```diff:app/app/views/todos/index.html.erb
<h1>Todos</h1>
- <%= render "form", todo: Todo.new %>
+ <div id="todo-new">
+   <%= render "form", todo: Todo.new %>
+ </div>
<hr>
<%= turbo_frame_tag "todos-table" do %>
  <table class="table table-bordered mt-2">
    <thead>
      <td>ID</td>
      <td>Content</td>
      <td></td>
    </thead>
    <tbody id="todos_tbody">
      <%= render partial: 'todo', collection: @todos, cached: true %>
    </tbody>
  </table>
  <%= paginate @todos, theme: 'twitter-bootstrap-4'%>
<% end %>
```


```diff:app/app/views/todos/create.turbo_stream.erb
<%= turbo_stream.append  "todos_tbody" do %>
  <%= render partial: 'todos/todo', locals: { todo: @todo } %>
<% end %>
+ <%= turbo_stream.update  "todo-new" do %>
+   <%= render partial: "todos/form", locals: { todo: Todo.new } %>
+ <% end %>
```
createが動いたあと、入力フォームを初期化する処理を追加します

### 確認
![1709023381.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/328f210a-f2cd-cdf6-92ba-4679ef881a4f.gif)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/8fbac6bf-459f-12f1-0ee6-99fba081bfe4.png)
コンソールログで、createのturbo_streamが動き、_form.html.erbを再描写してるのが分かります

# 完了処理
```diff:app/config/routes.rb
Rails.application.routes.draw do
-   resources :todos
+   resources :todos do
+     post :fix, on: :member
+   end
  get 'top/index'
  root 'top#index' # ←この行を修正
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
  # Can be used by load balancers and uptime monitors to verify that the app is live.
  get "up" => "rails/health#show", as: :rails_health_check

  # Defines the root path route ("/")
  # root "posts#index"
end
```

```diff:app/app/controllers/todos_controller.rb
class TodosController < ApplicationController
-   before_action :set_todo, only: %i[show edit update destroy]
+   before_action :set_todo, only: %i[show edit update destroy fix]

  ...
  
+   def fix
+     if @todo.complete == true
+       @todo.update(complete: false)
+     else
+       @todo.update(complete: true)
+     end
+   end

   ...

end
```


```erb:fix.turbo_stream.erb
<%= turbo_stream.replace dom_id(@todo) do %>
  <%= render partial: 'todos/todo', locals: { todo: @todo } %>
<% end %>
```


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

```

### 確認

![1709024542.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/3582c00c-caea-adea-2dba-2d0726ac4204.gif)
fixボタンが押されたら完了とし、行が塗られるようにしました。
また、再度fixボタンが押されたらキャンセルされるようにしています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/2ebb236e-ece2-d42e-ef46-6aba3935944f.png)
コンソールログで、fixのturbo_streamが動いてるのが分かります
レコード更新が走ってるせいか、少し処理に時間がかかっています

# さいごに
hotwireのみでtodoリストを作成してみましたが、予想以上に描写速度が速く、意外と使えるのでは？と思っています。（今回はvalidateを設けてないです）
コチラにhotwireについての知見が色々載っていましたので、とても勉強になりました（ありがとうございます）

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

今回bootstrapを使ってたので、tailwindを使ったバージョンだったり、他のパターン(esbuildなど)でも色々試してみたいと思います