はじめに
「お気に入り機能」といえば、js.erb
ファイルを作成してAjaxで切り替える実装が多いのですが、今回はTurboStreamでお気に入りの非同期処理を行うことにしました。
使用技術
- Ruby 3.0.2
- Rails 6.1.4
- turbo-rails 0.5.12
アイコン用にFontAwesomeを使っています。
前提(Turboではなくても共通の部分)
モデル
users -< likes >- articles
という関連になっています。
同一ユーザーが同じ記事をお気に入りすることはないので、一意制約をつけておきます。
1
2
3
4
5
6
|
class like < ApplicationRecord
belongs_to :user
belongs_to :article
validates :article_id, uniqueness: { scope: :user_id }
end
|
1
2
3
4
5
6
7
8
|
class Article < ApplicationRecord
has_many :likes, dependent: :destroy
has_many :liking_users, through: :likes, source: :user
def liked_by?(user)
liking_users.include?(user)
end
end
|
コントローラ
1
2
3
4
5
|
Rails.application.routes.draw do
resources :articles do
resource :like, only: %i[create destroy]
end
end
|
記事に対してお気に入りをする / しない用のアクションを定義します。
記事1つに対してお気に入りをするのは1度のみなので、resource
という風に単数形にします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class LikesController < ApplicationController
before_action :require_login
def create
@article = Article.find(params[:article_id])
current_user.likes.create!(article_id: @article.id)
end
def destroy
@article = Article.find(params[:article_id])
like = current_user.likes.find_by!(article_id: @article.id)
like.destroy!
end
end
|
同じユーザー・同じ記事に対するLikeは1つしかないことを利用しています。
ビュー
以下のようにしてお気に入りボタンの部分をパーシャルに分けておきます。
current_user
というスコープが大きい変数をパーシャルから呼び出したくないので、お気に入りしているかどうかの判定はbooleanとして外部から渡すことにします。
(この辺、ベストプラクティスはあるのだろうか?)
1
|
<%= render 'likes/like_button', article: @article, liked: article.liked_by?(current_user) %>
|
1
2
3
4
5
6
7
8
9
|
<% if liked %>
<%= link_to article_like_path(article), method: :delete do %>
<i class="fas fa-heart"></i>
<% end %>
<% else %>
<%= link_to article_like_path(article), method: :post do %>
<i class="far fa-heart"></i>
<% end %>
<% end %>
|
ここまでで、「お気に入りをしている場合、していない場合の表示と、ボタンを押したときのサーバーサイド側の処理はできました。
が、現在はお気に入り登録 / 解除した後のレスポンスがないため、リクエストを送った後は自力でブラウザを更新して反映する必要があります。
ここからやっと本題です。
ちなみに、Ajaxで処理する場合は、remote: true
を付けたりcreate.js.erb
を作成してレスポンス返すようにします。
非同期にしない場合は、普通にredirect_to
でパスを指定すればいいです。
TurboStreamでお気に入り登録 / 解除後の挙動を実装する
<turbo-frame>
要素で囲まれた部分がTurboStreamで作用する部分になる。
1
2
3
4
5
6
7
8
9
10
11
|
<turbo-frame id="like-button">
<% if liked %>
<%= link_to article_like_path(article), method: :delete do %>
<i class="fas fa-heart"></i>
<% end %>
<% else %>
<%= link_to article_like_path(article), method: :post do %>
<i class="far fa-heart"></i>
<% end %>
<% end %>
</turbo-frame>
|
コントローラではrenderでTurboStreamの挙動を記述することができる。
今回は、replaceを使うことでパーシャルごと再描画する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
class LikesController < ApplicationController
before_action :require_login
def create
@article = Article.find(params[:article_id])
current_user.likes.create!(article_id: @article.id)
render turbo_stream: turbo_stream.replace(
'like-button',
partial: 'likes/like_button',
locals: { product: @product, liked: true },
)
end
def destroy
@article = Article.find(params[:article_id])
like = current_user.likes.find_by!(article_id: @article.id)
like.destroy!
render turbo_stream: turbo_stream.replace(
'like-button',
partial: 'likes/like_button',
locals: { product: @product, liked: false },
)
end
end
|
これによって、<turbo-frame id="like-button">
がパーシャルの中身のHTMLで置き換えられる(<turbo-frame>の中が ではなく <turbo-frame>要素ごと 置き換えられるので注意)。
なので、お気に入り登録(create)をしたときのレスポンスとその後のhtmlは以下のようになる。
1
2
3
4
5
|
<turbo-frame id="like-button">
<a rel="nofollow" data-method="delete" href="/articles/33/like">
<i class="fas fa-heart" aria-hidden="true"></i>
</a>
</turbo-frame>
|
参考