現在絶賛作成中のポートフォリオでいいね機能を実装する際、色々躓いてしまったので反省もかねてメモしとこうと思います。
#環境
- Rails 5.2.2
- mysql 8.0.17
- macOS
#実現したいこと
Twitterのようないいね機能の実装。
##そのために必要な処理
いいねボタンを押下した場合、
- 「いいねしたユーザーのid:user_id」と「いいねされた投稿のid:post_id」が「中間テーブル:likesテーブル」に保存される。
- いいねボタンが「いいね追加」と「いいね削除」で切り替わる。
##必要手順
- 中間テーブルの作成
- テーブルの紐付け
- ルーティングの設定
- コントローラー側の処理
- ビューでの表示処理
大体こんな感じです。
では順番に見ていきましょう。
#中間テーブルの作成
まずは、「いいねしたユーザー」と「いいねされた投稿」を保存するテーブルの作成からです。
$ rails g model Like user:references post:references
referencesを指定することで、外部キー制約が自動で付きます。
非常に便利。
問題なければこのまま
$ rails db:migrate
しちゃいましょう。
#テーブルの紐付け
お次はモデルでhas_manyを使ってテーブルを紐付けます。
これに関しては、
【Rails】投稿とユーザーの紐付け(一対多)のメモ
でも似たような記事を書きましたので、復習になります。
has_many :likes
has_many :users, through: :likes
def liked_by?(user)
likes.where(user_id: user.id).exists?
end
いいねされた投稿に対して、いいねしたユーザーは複数いるのでhas_manyを使います。
ここで使われているthroughは、likeテーブルを経由して直接userテーブルと繋げるためのものです。
また、
def liked_by?(user)
likes.where(user_id: user.id).exists?
end
この子は「すでにいいねしたかどうか」を判断するためのメソッドで、後にビュー側で必要となってきます。
一応簡単に説明すると、
likesテーブルの「いいねしたユーザー:user_id」カラムにuser.idが存在するのか探すという処理です。
引数userにはビューでcurrent_userを指定して入れます。
has_many :likes, dependent: :destroy
has_many :like_posts, through: :likes, source: :post
ユーザー側でもしてることはほとんど変わりません。
一点気をつけるとすれば、
has_many :like_posts, through: :likes, source: :post
の部分です。
多くの関連記事でlike_postsのところがpostsになっていたのですが、もし他に has_many :posts があるなら名前が被らないようにしましょう。
railsが混乱してエラーになってしまうので、like_postsのように名前が被らないように注意することをおすすめします。
#ルーティングの設定
お次はルーティングです。
resources :posts do
post 'add' => 'likes#create'
delete '/add' => 'likes#destroy'
end
postのidをとってくるためにネストします。
中は普通にルーティング設定してあげればおkです。
#コントローラー側の処理
ここまできたらあともう少し。
処理を書くためにコントローラーを作成します。
$ rails generate controller likes
コントローラーができたら以下の処理を記述します。
class LikesController < ApplicationController
before_action :authenticate_user!
before_action :set_like
def create
user = current_user
post = Post.find(params[:post_id])
like = Like.create(user_id: user.id, post_id: post.id)
end
def destroy
user = current_user
post = Post.find(params[:post_id])
like = Like.find_by(user_id: user.id, post_id: post.id)
like.delete
end
private
def set_like
@post = Post.find(params[:post_id])
end
end
create/destroyアクションで共通する処理は以下になります。
user = current_user
post = Post.find(params[:post_id])
- いいねするユーザーであるcurrent_userを変数userに格納
- いいねされた投稿のidとPostテーブルのidが一致するものをfindで見つけて変数postに格納
##createアクション
createアクションでは、いいねされた場合の処理を記述します。
like = Like.create(user_id: user.id, post_id: post.id)
Likeテーブルに、
user_idが、先ほどcurrent_userを格納した変数userのidで、post_idが、いいねされたPostテーブルのidを格納した変数post
のデータをcreateで作成する処理です。
##destroyアクション
destroyアクションでは、いいねが取り消された場合の処理を記述します。
like = Like.find_by(user_id: user.id, post_id: post.id)
like.delete
find_byで、user_idがcurrent_userのidと一致するもの且つ、post_idがいいねされたpostのidと一致するものを探して、変数likeに格納します。
そしてdeleteメソッドでlikeを削除。
##set_post
地味に一番下のところに、
private
def set_like
@post = Post.find(params[:post_id])
end
とありますが、こちらはjsのところで必要となってきますのでひとまずおいといてください。
#ビュー側の処理
index.html.erbでは投稿一覧を表示しています。
この投稿一つ一つにいいねぼたんを表示させたいわけですが、そのためにはいくつか階層を分ける必要があります。
とりあえず、いいねボタンを表示したいところに以下のように記述しましょう。
<div id="like-btn-<%= post.id %>">
<%= render 'likes/like', post: post %>
</div>
renderを使用することで、いいねだけを表示するviewを表示します。
post: post は、いいねだけを表示するviewのpostにpost(ここでは@postsをeachで回してます)の情報を入れるでというくらいの意味あいだと理解して問題ないかと。
例えば、postの情報とは違った情報も渡したいのであれば、それに適応したものを渡してあげたらおk(私の場合はブックマーク一覧のviewでも同じようにいいねできるようにしたかったので、別にviewを作成してブックマークされている投稿の情報を格納したlikeを post: like として設定していました)。
##_like.html.erb
「いいねだけを表示するview」である_like.html.erbを作成します。
<% if post.liked_by?(current_user) %>
<%= link_to(post_add_path(post), method: :delete, remote: true, id: :"like-button-#{post.id}") do %>
<i class="fa-lg fas fa-heart icon-btn liked"></i>
<% end %>
<% else %>
<%= link_to(post_add_path(post), method: :post, remote: true, id: :"like-button-#{post.id}") do %>
<i class="fa-lg fas fa-heart icon-btn not-like"></i>
<% end %>
<% end %>
###if post.liked_by?(current_user)
まずこのif文は、先ほどmodelで定めたメソッドを使ってすでに現在のユーザーがいいねしてるかどうかを判定します。
- すでにいいねしてる場合 → link_toはmethod: :delete
- まだいいねしてない場合 → link_toはmethod: :post
となるわけですね。
###remote: true
link_toに記述しているremote: trueですが、この子はコントローラーに値を送信する役割を果たし、ajaxを発火してくれる優秀な子です。
ajaxを利用することで、画面全体をリダイレクトする必要がなく、いいねアイコンの部分のみ更新してくれるので、アプリ全体の負担が減ります。
では最後に、remote: trueで呼び出すためのjsファイルを作ってあげましょう。
##create.js.erb/destroy.js.erb
_like.html.erbと同階層に次の二つのファイルを作成します。
$('#like-btn-<%= @post.id %>').html("<%= escape_javascript(render partial: "likes/like", locals: { post: @post }) %>");
$('#like-btn-<%= @post.id %>').html("<%= escape_javascript(render partial: "likes/like", locals: { post: @post }) %>");
内容はどっちも一緒です。
先ほどコントローラー側で設定したset_likeで、いいねされた投稿のidを取得して、それをいいねアイコンのidとしています。
ここまでできたら、いいねボタンの完成です!
あとはいいねしたときと外した時でスタイルを変更するなど、創意工夫してください。
#まとめ
いいね機能はほとんどのアプリで使うと思うので、結構情報があったのですが、いまいち仕組みが理解ができなくて結構苦戦しました。
特に中間テーブルへの保存で時間を取られたと思います。
できる範囲で細かく書いたので、誰かの助けになれば幸いです。
ではでは。