概要
いいね機能の実装方法についてまとめます。
#参照
同期通信のいいね機能実装は以下の本を参考に、その後の非同期通信化は以下の記事を参考にして行いました。
ありがとうございます。
本
記事
【Rails】いいね機能完全版!同期いいね、いいね数の表示、非同期いいね、アイコン表示、それぞれの実装方法についてまとめて解説
完成イメージ
今回は、写真投稿アプリを題材にいいね機能を実装する方法をまとめます。
開発環境
macOS Catalina 10.15.7
ruby 2.6.5
Rails 6.0.3.4
実装の流れ
1.同期通信いいねの実装
1. 中間テーブルモデルの作成
2. モデルの関連付け
3. いいねルールをメソッドに設定
4. ルーティングを設定
5. いいねボタンを作成
6. いいねアクションをコントローラーに設定
2.いいね機能の非同期通信化
1. link_toの引数にremote: trueを付与
2. いいねアクション後の処理をredirect_toからrenderに変更
3. いいねボタン用部分テンプレートの外枠ブロックにidを付与
4. js.erbファイルを作成
今回のコード
コードは必要な部分のみ記載します。
CSSコードなどは省略してあります。
1.同期通信いいねの実装
1. 中間テーブルモデルの作成
いいねをしたuserのidと、いいねをされたphotoのidを保存するためのテーブルとして、votesという中間テーブル(Voteモデル)を作成します。
% rails g model vote
class CreateVotes < ActiveRecord::Migration[6.0]
def change
create_table :votes do |t|
t.references :photo, null: false, foreign_key: true
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
% rails db:migrate
2. モデルの関連付け
モデル間のアソシエーションを以下のように設定します。
class Vote < ApplicationRecord
belongs_to :user
belongs_to :photo
(以下省略)
class User < ApplicationRecord
has_many :votes, dependent: :destroy
has_many :voted_photos, through: :votes, source: :photo
(以下省略)
dependent: :destroyとすることで、userが削除されると、そのuserに関連したvotesテーブルのレコードも削除されます。
has_many :photosとすると、votesを介した関連性がわかりにくくなるため、かわりに:voted_photosという名前を用いています。その後、:voted_photosは:photoモデルの:photosだと設定するためにsourceオプションを使っています(説明がわかりづらい)。
class User < ApplicationRecord
has_many :votes, dependent: :destroy
has_many :voters, through: :votes, source: :user
(以下省略)
こちらも同様に、:userのかわりに:votersという名前を用いています。
3. いいねルールをメソッドに設定
ユーザーがいいねボタンを押せるか判断するためのメソッドをUserモデルに作成します。
具体的には以下のルールを設定します。
①自分が投稿した写真にはいいねできない
②1つの写真には1回しかいいねできない
class User < ApplicationRecord
(省略)
def deletable_vote?(photo)
photo && photo.user != self && !votes.exists?(photo_id: photo.id)
end
(以下省略)
『photoが存在する』かつ『photoのuserはログインしているuserではない』かつ『ログインuserはこのphotoにいいねしていない』が真であればtrueが返されます。
selfをコンソールで確認するとログインしているユーザーの情報が格納されています。
加えて、ユーザーがいいねボタンを解除できるか判断するためのメソッドもUserモデルに作成します。
『photoが存在する』かつ『photoのuserはログインしているuserではない』かつ『ログインuserはこのphotoにいいねしている』が真であればtrueが返されます。
class User < ApplicationRecord
(省略)
def votable_for?(photo) # ←いいなボタンを押せるか判断
photo && photo.user != self && !votes.exists?(photo_id: photo.id)
end
def deletable_for?(photo) # ←いいねボタンを解除できるか判断
photo && photo.user != self && votes.exists?(photo_id: photo.id)
end
(以下省略)
Voteモデルにvalidateメソッドを設定して、userが上記ルールに合わない場合はいいねが保存されないようにします。
class Vote < ApplicationRecord
belongs_to :user
belongs_to :photo
validate do
unless user && user.votable_for?(photo)
errors.add(:base, :invalid)
end
end
end
4. ルーティングを設定
Rails.application.routes.draw do
(省略)
resources :photos do
(省略)
patch "like", "unlike", on: :member
get "voted", on: :collection
(以下省略)
end
likeアクション:いいねボタンをクリックするとvotesテーブルにレコードが保存される
unlikeアクション:いいねボタンをクリックするとvotesテーブルからレコードが削除される
votedアクション:自分がいいねした写真の一覧を表示する(今回は説明しません)
5. いいねボタンを作成
いいねボタン用の部分テンプレートを作成します。
上記で作成したいいねルールをもとにボタンをかえて表示させます("いいね"または"いいねを解除")
<div>
<% if current_user && current_user.votable_for?(photo) %>
<%= link_to "いいね" like_photo_path(photo), method: :patch %>
<% elsif current_user && current_user.deletable_for?(photo) %>
<%= link_to "いいねを解除", unlike_photo_path(photo), method: :patch %>
<% end %>
</div>
今回は写真詳細ページで、いいねボタン用の部分テンプレートをrenderで読み込みます。こうすることで、写真詳細ページにいいねボタンが設置されます。photo: @photoとすることで部分テンプレート内のローカル変数photoにインスタンス変数@photoをわたします。@photoはこの後photosコントローラー内で作成します。
(省略)
<div>
<%= render partial: 'shared/votes', locals: { photo: @photo } %>
</div>
(以下省略)
6. いいねアクションをコントローラーに設定
class PhotosController < ApplicationController
before_action :set_photo, only: [(省略), :like, :unlike]
(省略)
# いいね
def like
current_user.voted_photos << @photo
redirect_to photo_path
end
# いいね削除
def unlike
current_user.voted_photos.destroy(@photo)
redirect_to photo_path
end
private
(省略)
def set_photo
@photo = Photo.find(params[:id])
end
(以下省略)
current_user.voted_photos << @photo
とすることで、current_userのuser_idと@photoのphoto_idがvotesテーブルに保存されます。
また
current_user.voted_photos.destroy(@photo)
とすることで、current_userのuser_idと@photoのphoto_idがvotesテーブルから削除されます。
どちらも、usersテーブルとphotosテーブルのレコードには影響を与えません。
以上で、同期通信のいいねボタンが実装できました。
↓挙動確認(いいねボタンを押すたびに、左上の再読み込みボタンがXになっているのが確認できます)
2.いいね機能の非同期通信化
ここからは、いいね機能を非同期通信化します。
1. link_toの引数にremote: trueを付与
いいねボタン用の部分テンプレートを修正します。
<div>
<% if current_user && current_user.votable_for?(photo) %>
<%= link_to "いいね" like_photo_path(photo), method: :patch, remote: true %>
<% elsif current_user && current_user.deletable_for?(photo) %>
<%= link_to "いいねを解除", unlike_photo_path(photo), method: :patch, remote: true %>
<% end %>
</div>
"いいね"と"いいねを解除"のlink_toの引数にremote: trueを付与します。
これで非同期通信機能を実装することができます。
2. いいねアクション後の処理をredirect_toからrenderに変更
(省略)
# いいね
def like
current_user.voted_photos << @photo
render 'vote.js.erb' # renderメソッドを追加
# redirect_to photo_path, notice: "いいねしました。←削除"
end
# いいね削除
def unlike
current_user.voted_photos.destroy(@photo)
render 'vote.js.erb' # renderを追加
# redirect_to photo_path, notice: "削除しました。←削除"
end
(以下省略)
likeアクションまたはunlikeアクションの処理後に、render 'vote.js.erb'とすることで、vote.js.erbファイルが実行されます。vote.js.erbはこの後作成します。
3. いいねボタン用部分テンプレートの外枠ブロックにidを付与
(省略)
<div id="vote-box"> # ←idを付与
<%= render partial: 'shared/votes', locals: { photo: @photo } %>
</div>
(以下省略)
この後作成するjs.erb
ファイル内で、いいねボタン用部分テンプレートの外枠ブロック(div)のidをもとに、この外枠ブロックを取得し、ブロック内の部分テンプレートを再読み込みさせます。"vote-box"というidを付与しました。
今回は、写真詳細画面(1投稿分のみ表示)にいいねボタンを設置したため、部分テンプレート外枠のidは"vote-box"のみのひとつとしています。もし写真一覧ページなどで各写真にいいねボタンを設置する場合は、各いいねボタンの外枠ブロックごとに異なるidを付与する必要があります。その場合は、コントローラー内で各いいねボタンの外枠ブロックごとに異なるid名が作成されるようにするなど工夫が必要です(例えば、@id_name = "vote-box-#{@photo.id}"など。詳細な説明は省略します)。
4. js.erbファイルを作成
document.getElementById("vote-box").innerHTML = '<%= escape_javascript(render partial: 'shared/votes', locals: { photo: @photo }) %>'
このコードで、いいねボタン用の部分テンプレートを再読込します。
もし、"いいね"状態のボタンをクリックした場合は、"いいね解除"が表示されるようになります。
また、"いいね解除"状態のボタンをクリックした場合は、"いいね"が表示されるようになります。
以上で、非同期通信のいいねボタンが実装できました。
↓挙動確認(いいねボタンを押しても、左上の再読み込みボタンは変わりません)
おわりに
もし、いいねボタンをアイコン化したい場合は、以下のようにFontAwsomeを利用することでできます。
<div>
<% if current_user && current_user.votable_for?(photo) %>
<%= link_to like_photo_path(photo), method: :patch, remote: true do %>
<i class="far fa-heart"></i>
<% end %>
<span><%= photo.votes.count %></span> # ←いいねの数も表示させています
<% elsif current_user && current_user.deletable_vote?(photo) %>
<%= link_to unlike_photo_path(photo), method: :patch, remote: true do %>
<i class="fas fa-heart"></i>
<% end %>
<span><%= photo.votes.count %></span> # ←いいねの数も表示させています
<% end %>
</div>
FontAwsomeの使い方については以下の動画などをご参照ください。
FontAwsomeのgemもあるのですが、gemだとうまく導入できなかったため、FontAwsomeのホームページからKit's Codeをapplication.html.erbにコピペしてFontAwsomeを導入しました(以下動画参照)。
【Ruby on Rails】Font Awesomeをrailsのボタンに導入する Adding Font Awesome to Ruby on Rails
【Font Awesome】基本的な使い方(導入〜アニメーション)初心者向け Font Awesome tutorial
間違いや改善点などありましたらご指摘お願いいたします。