1. kazutosato

    Posted

    kazutosato
Changes in title
+Hotwire(Turbo)を試す その2: Trubo Streamsでページの一部の差替・追加
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,225 @@
+環境: Rails 6.1、Turbo 7.0.0-beta.5
+
+[その1](https://qiita.com/kazutosato/items/10a5bc04443d6b7e5bf8)の続き
+
+## Turbo Streamsとは
+
+Turbo Streamsとは、サーバーからHTMLの一部を送信して、ページ中の一部を差替、追加、削除するものです。送信の方法には、普通のHTTPのレスポンスのほかにAction Cableが使えます。この「その2」のサンプルは、普通のHTTPのレスポンスを使うだけです。
+
+ページ中に次のような`<turbo-frame>`要素があるとします。id属性で名前を付けます。
+
+```html:現在のページ中のHTMLの一部
+<turbo-frame id="message">
+ <div>こんにちは</div>
+</turbo-frame>
+```
+
+これが含まれたページに対して、次のような`<turbo-stream>`要素のHTML片を送信します。target属性で差替対象となる`<turbo-frame>`の名前を指定します。差替なのでaction属性は"replace"です。`<turbo-stream>`の内容は、`<template>`で囲む必要があります。
+
+```html:送信するHTML片
+<turbo-stream action="replace" target="message">
+ <template>
+ <div>こんばんは</div>
+ </template>
+</turbo-stream>
+```
+
+HTML片を送信すると、`<turbo-frame>`の内容が入れ替わります。
+
+```html:送信後のページ中のHTML
+<turbo-frame id="message">
+ <div>こんばんは</div>
+</turbo-frame>
+```
+
+## 「いいね」ボタンの例
+
+[その1](https://qiita.com/kazutosato/items/10a5bc04443d6b7e5bf8)で作ったサンプルに「いいね」ボタンを追加してTurbo Streamsを試します。記事の詳細ページの中に❤️ボタンを置き、クリックするとカウンタが1上がる、ということにします。
+
+テーブルにカウンタを保存するカラムを追加します。
+
+```ruby:db/migrate/20210419022554_add_likes_count_to_entries.rb
+class AddLikesCountToEntries < ActiveRecord::Migration[6.1]
+ def change
+ add_column :entries, :likes_count, :integer
+ end
+end
+```
+
+いいね用のアクションを追加します。
+
+```ruby:config/routes.rb
+ resources :entries do
+ patch :like, on: :member
+ end
+```
+
+記事ページに埋め込む❤️ボタンとカウンタの数字です。`turbo_frame_tag "名前"` メソッドは `<turbo-frame id="名前">〜</turbo-frame>` になります。
+
+```erb:app/views/entries/_likes.html.erb
+<%= turbo_frame_tag "entry-likes" do %>
+ <div>
+ <%= link_to '❤️', like_entry_path(@entry), method: :patch %>
+ <span style="color:red"><%= @entry.likes_count %></span>
+ </div>
+<% end %>
+```
+
+これを記事ページに埋め込みます。
+
+```erb:app/views/entries/show.html.erb
+<%= render 'likes' %>
+```
+
+クリックで呼び出されるコントローラのいいね用アクションです。
+
+```ruby:app/controllers/entries_controller.rb
+ def like
+ @entry.increment!(:likes_count)
+ render turbo_stream: turbo_stream.replace('entry-likes', partial: 'likes')
+ end
+```
+
+`render turbo_stream: turbo_stream.replace('名前', partial: 'テンプレート')`は、次のようなHTML片を送信し、`<turbo-frame id="名前">`の内容を差し替えます。このとき、HTTPレスポンスのContent-typeは、`text/vnd.turbo-stream.html` になります。
+
+```html
+<turbo-stream action="replace" target="名前">
+ <template>
+ partialのテンプレート
+ </template>
+</turbo-stream>
+```
+
+❤️をクリックすると数字が増えるようになりました。
+![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308595/cfa26c9e-c7ca-976f-763c-de087003fd83.png)
+Turbo Streamsのテンプレートと送信には、もう1つやり方があります。次のように`<turbo-stream>`をテンプレートの中に書く方法です。`turbo_stream.replace "名前"` は `<turbo-stream action="replace" target="名前"><template>〜</template></turbo-stream>` になります。
+
+```erb:app/views/entries/like.html.erb
+<%= turbo_stream.replace 'entry-likes' do %>
+ <%= render 'likes' %>
+<% end %>
+```
+
+このテンプレートをContent-typeを指定して送信すると、同じ結果になります。
+
+```ruby:app/controllers/entries_controller.rb
+ def like
+ @entry.increment!(:likes_count)
+ render layout: false, content_type: "text/vnd.turbo-stream.html"
+ end
+```
+
+## 「もっと見る」ボタンの例
+
+差替の次は、コンテンツの追加を試すために、記事一覧に「もっと見る」ボタンを実装します。まず、一覧のテンプレートを修正します。scaffoldのテンプレートはテーブルを使っているため、Turbo StreamsでHTML要素をうまく扱えません。divに変えます。
+
+追加のターゲットになる記事の一覧は、`<turbo-frame>`で囲み、名前を"entries"としています。
+
+```erb:app/views/entries/index.html
+<div>
+ <%= turbo_frame_tag "entries" do %>
+ <% @entries.each do |entry| %>
+ <%= render 'entry', entry: entry %>
+ <% end %>
+ <% end %>
+</div>
+
+<br>
+<%= render 'more_button' %>
+```
+
+```erb:app/views/entries/_entry.html
+<div class="entry">
+ <%= link_to entry.title, entry %> |
+ <%= entry.created_at.strftime('%m/%d %H:%M') %> |
+ <%= link_to 'Edit', edit_entry_path(entry) %> |
+ <%= link_to 'Destroy', entry, method: :delete, data: { confirm: 'Are you sure?' } %>
+</div>
+```
+
+「もっと見る」ボタンのテンプレートです。10個ずつ記事を表示し、すべて表示されたらボタンを非表示とします。「もっと見る」も`<turbo-frame>`で囲み、名前を"more-button"としています。
+
+```erb:app/views/entries/_more_button.html
+<% if @offset + 10 < @entries_count %>
+ <%= turbo_frame_tag "more-button" do %>
+ <div>
+ <%= link_to 'もっと見る', more_entries_path(offset: @offset + 10) %>
+ </div>
+ <% end %>
+<% end %>
+```
+
+「もっと見る」用のルーティングです。
+
+```ruby:config/routes.rb
+ resources :entries do
+ get :more, on: :collection
+ patch :like, on: :member
+ end
+```
+
+コントローラの記事一覧と「もっと見る」用のアクションです。記事は10個ずつ、offsetパラメータの位置から取得、作成日時の降順、とします。
+
+moreアクションでは、`render turbo_stream: 〜` を使わない形にします。
+
+```ruby:app/controllers/entries_controller.rb
+ def index
+ @entries_count = Entry.count
+ @offset = params[:offset].to_i
+ @entries = Entry.offset(@offset).order(created_at: :desc).limit(10)
+ end
+
+ def more
+ index
+ render layout: false, content_type: "text/vnd.turbo-stream.html"
+ end
+```
+
+「もっと見る」用のTurbo Streamsのテンプレートです。ここでは`<turbo-stream>`要素を2つ送信していいます。記事一覧の追加分と「もっと見る」の差替分です。追加するときは、replace の代わりに append を使います。
+
+```erb:app/views/entries/more.html
+<%= turbo_stream.append "entries" do %>
+ <% @entries.each do |entry| %>
+ <%= render 'entry', entry: entry %>
+ <% end %>
+<% end %>
+
+<%= turbo_stream.replace "more-button" do %>
+ <%= render 'more_button' %>
+<% end %>
+```
+
+記事の数を増やして試すためにシードデータを用意します。
+
+```ruby:db/seeds.rb
+32.times do |idx|
+ entry = Entry.create!(
+ title: %w(foo bar baz).sample(3).join(' '),
+ body: 'blah, blah, blah...',
+ created_at: idx.hours.ago
+ )
+end
+```
+
+「もっと見る」をクリックすると記事が10個ずつ追加され、何度もクリックすると「もっと見る」が消えるようになります。
+![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308595/edb2ce72-412f-89b5-007f-2a6a333648b0.png)
+
+## turbo-frame の中のリンクの挙動
+
+さて、この記事一覧の中のリンクをクリックすると、ページが切り替わらずに、記事一覧、つまり`<turbo-frame id="entries">〜</turbo-frame>`で囲んだ部分が消えてしまいます。
+
+`<turbo-frame id="名前">`の中にあるリンクは、レスポンスのHTMLにも`<turbo-frame id="名前">`があることを期待し、元の`<turbo-frame>`を新しい`<turbo-frame>`で置き換える、というのがデフォルトの動作になっています。
+
+この動作を変えて別のページに切り替わるようにするには、`<turbo-frame>`に属性`target="_top"`を加えます。
+
+```erb:app/views/entries/index.html
+ <%= turbo_frame_tag "entries", target: "_top" do %>
+```
+
+## ここまでの感想
+
+- Turboでこのサンプルを実装するのは、けっこう面倒でした。実際のアプリケーションに導入するには学習コストが気になります。まだドキュメントも解説サイトも揃っていない段階ではしょうがありませんが。
+- 実際に導入するときは、Turbo+Stimulusの簡単な部分だけ使い、Vue.jsやjQueryと組み合わせるのがいいかもしれない。
+
+
+