いいね機能とは
ユーザーが投稿した記事や商品に対して、「いいね」をつけます。
以下が完成形です。
実装方法
今回はRails、Hamlという環境で実装します。
ページのリロードはせず、非同期通信で画面を変化させます。
ポイントは以下の通りです。
・ユーザーはひとつの記事や商品に対し、一度しか「いいね」できない。
・「いいね」するとボタンの色が変わり数字が増え、もう一度押すと取り消され元のボタンに戻る。
実装の準備
では、順をおって実装していきましょう。
今回はフリーマーケットサイトにおいて、ユーザーが出品した商品に対しての「いいね」になります。
各モデルの準備
ユーザー(user)が商品(product)に対し「いいね(like)」するので、
・users
・products
・likes
以上三つのモデル、テーブルが必要です。
ですが、今回は「いいね」の実装ということで、userとproductがすでに存在している前提でのお話になります。
いいね(like)は、それをしたユーザー(user)とそれをされた商品(product)に紐づいているので、それに応じたアソシエーションを組む必要があります。
では、それぞれをつくっていきましょう。
$ rails g model like
Modelの命名規則は単数形なので、ここでも単数形で書くのが良さそうです(複数形で書いても勝手に単数形モデルになりますが・・・)。
アソシエーションを組む
ユーザー(user)と商品(product)は「いいね(like)」をたくさん持っていて、「いいね(like)」はそれらに属しています。
ですので、カラムをつくるときは、likesテーブルに「user_id」と「product_id」を持たせます。
class CreateLikes < ActiveRecord::Migration[5.2]
def change
create_table :likes do |t|
t.references :product
t.references :user
t.timestamps
end
end
end
$ rake db:migrate
アソシエーションは、
class User < ApplicationRecord
has_many :likes, dependent: :destroy
end
class Product < ApplicationRecord
has_many :likes, dependent: :destroy
end
class Like < ApplicationRecord
belongs_to :product
belongs_to :user
validates :user_id, presence: true
validates :product_id, presence: true
validates_uniqueness_of :product_id, scope: :user_id
end
「dependent: :destroy」
userやproductが削除された際は、それに関連するlikeも一緒に消してねという意味です。
「validates :user_id, presence: true」「validates :product_id, presence: true」
likeを生成するときはuser_idとproduct_idが空ではだめだよというバリデーション。
「validates_uniqueness_of :product_id, scope: :user_id」
ひとつのproductにひとりのuserが複数回いいねできないよう、product_idとuser_idの組み合わせはひとつだけだよというバリデーションです。
とりあえずの紐づけができました。
Likesコントローラーの作成
コントローラーも作ります。
「いいね(like)」は「生成」と「削除」のふたつが想定されるので、createアクションとdestroyアクションがありますね。
$ rails g controller likes
コントローラーの命名規則は複数形です。
class LikesController < ApplicationController
def create
end
def destroy
end
end
ルーティング
「いいね(like)」はproductに対してのもので、likesコントローラーで持ち主のproduct_idが必要なので、productにネストさせます。
しかし、今回はすでにproductがuserにネストされているので、禁断の2段階ネストになっています。
resources :users, only: [:show, :edit, :update] do
resources :products, only: [:new, :create, :show] do
resources :likes, only: [:create, :destroy]
end
end
ここまでが準備です。
実装する
では、実装していきましょう。
まず、全体のイメージをざっくり説明です。
実装は大きく分けて三つ、
1.ユーザーがすでに「いいね」しているかどうかで、ボタンのスタイルが違う。
2.ボタンを押したら数字が増え、もう一度押すと数字が減る。
3.ボタンが押されるというアクションによって、ボタンの状態を切り替える。
まず、1
これはビューにおいてボタンのスタイルを2つつくり、ifを使って場合分けします。
次に、2
「いいね」の数はテーブルにあるレコードの数なので、「いいね」するとレコードが増え、その分数字が増える。減るのは、レコードが削除されるため。
最後に、3
これは非同期通信によるものです。
具体的には、1で説明したビューが部分テンプレートになっており、ボタンが押されたことにより、この部分テンプレートに送る値や条件を変化させています。
この説明ではわかりづらすぎるので、実際に手順を追っていきます。
カウント機能の作成
likesテーブルにあるproduct_idごとのレコードを集計します。
まずは、likeモデルに追加。
class Like < ApplicationRecord
#counter_cache: :likes_count を追加
belongs_to :product, counter_cache: :likes_count
belongs_to :user
end
「counter_cache: :likes_count」
リレーションされたlikeの数を数え、productのlikes_countカラムに入れるという意味。likeがcreateされるとlike_countが+1、destroyされると-1します。
よって、productsテーブルにlikes_countカラムを追加しなければなりません。
$ rails g migration AddClomunToLikes
class AddClomunToLikes < ActiveRecord::Migration[5.2]
def change
add_column :products, :likes_count, :integer
end
end
$ rake db:migrate
実際に「いいね」していくと、このように数字がカウントされます。
ビューの作成
ビューの作成ですが、まずはユーザーがすでに「いいね」しているのかどうかのメソッドをつくります。
class Product < ApplicationRecord
...
#以下を追加
def like_user(user_id)
likes.find_by(user_id: user_id)
end
end
引数として渡されたuser_idが、likesテーブルのuser_idカラムにすでに存在しているかを確かめ、true、falseを返します。
...
.item-button-container
.item-button-container__left
= render partial: 'likes/like', locals: { product: @product, products: @products, likes: @likes, like: @like}
...
#ユーザーがサインインしているかどうか
- if user_signed_in?
#ログインしているユーザーがすでに「いいね」しているかどうか
- if product.like_user(current_user.id)
.item-button-container__left__dislike
=link_to user_product_like_path(current_user, product, like), method: "DELETE", remote: true do
%i.fas.fa-heart
%span いいね!
%span
= product.likes_count
- else
.item-button-container__left__like
=link_to user_product_likes_path(current_user, product), method: "POST", remote: true do
%i.fas.fa-heart
%span いいね!
%span
= product.likes_count
-else
.item-button-container__left__like
%i.fas.fa-heart
%span いいね!
%span = product.like_count
「= product.likes_count」で、さきほどのいいねのカウント数が表示されます。
Ajax通信
ボタンを押すというアクションによってAjax通信をします。
といっても、link_toメソッドにremote: trueを記述しているので、jsファイルに通信のあれこれを定義することはありません。すでにこのlink(今回のボタン)はAjaxになっています。
「いいね」を押した先のコントローラー
class LikesController < ApplicationController
def create
@like = Like.create(user_id: current_user.id, product_id: params[:product_id])
@likes = Like.where(product_id: params[:product_id])
get_product
end
def destroy
@like = Like.find_by(user_id: current_user.id, product_id: params[:product_id])
@like.destroy
@likes = Like.where(product_id: params[:product_id])
get_product
end
def get_product
@product = Product.find(params[:product_id])
end
end
ボタンによってcreate、destroyをするとlikes_countの数値が変わるので、数値の変わったlikesとproductを改めて取得してビューに渡します。
最後に、これらの値が渡ったjsファイルをつくります。
$(".item-button-container__left").html("#{escape_javascript(render partial: 'like', locals: { product: @product, products: @products, likes: @likes, like: @like})}")
$(".item-button-container__left").html("#{escape_javascript(render partial: 'like', locals: { product: @product, products: @products, likes: @likes, like: @like})}")
ここでは部分テンプレートのみを書き換えています。
つまり、部分テンプレートに渡す値を「いいね」が追加、削除された後のものに置き換えているということです。
これにより、現在のユーザーの「いいね」も追加されたり削除されるので、条件分岐によりボタンのスタイルが変わります。
まとめ
Ajax通信で部分テンプレートを差し替えるというテクニックは他にも応用ができそうです。
仕組みが複雑で説明がとても難しく散漫な記事となってしまいましたので、参考記事とともにご覧いただければ幸いです。
https://qiita.com/YuitoSato/items/94913d6a349a530b2ea2
https://www.prime-architect.co.jp/myblog/ruby-on-rails-1559