何をしたか
Railsの課題を実施しています。その中で「いいね!」機能をAjaxで作成しましょうというタスクがありました。
実は以前、非同期通信ではない「いいね!」機能は作成したことがありました。
▼その時の記録はこちら
Railsで「いいね!」機能を作る - ①アソシエーションに別名をつける
Railsで「いいね!」機能を作る - ②「いいね!」のcreateアクション
Railsで「いいね!」機能を作る - ③「いいね!」を解除できるようにする
また、別の記事で**「いつかAjaxで”いいね!”機能を作ってみたい」**と言いつつ、作っていなかったので、作成の手順をノートにまとめたいと思います。
なお、ここではメモ程度に実装手順を紹介しています。実際に詳しい実装の手順は↑上記の記事をご覧ください。
また、実装環境は以下の通りです。
Rails 5.2.3
Ruby 2.6.0
仕様確認・定義する
「いいね!」機能はアソシエーションの定義に若干ひねりが必要です。今回もまずはDB構造と仕様を確認します。その結果、DB構造と仕様は下記の通りでした。
※images
テーブルは画面外にあります。
※他にもあるテーブルの中の一部を表示しています。
このうち、**「自分の投稿にいいね!できない」**というのは、下記のように、自分の投稿の時にはビューに「いいね!」ボタンを表示しない、という形で実現しています。
また、**like_posts
**と赤文字で書いてあるのは、この後記載するアソシエーションの別名です。
「アソシエーションの別名って???」という方は、こちらの記事に詳しく説明してあります。
Railsで「いいね!」機能を作る - ①アソシエーションに別名をつける
マイグレーションの作成
では、ここから実装していきます。まずは、マイグレーションファイルを作成します。
users
テーブルとposts
テーブルはそれぞれ作成済という前提です。
likes
テーブルは下記のように作成します。
class CreateLikes < ActiveRecord::Migration[5.2]
def change
create_table :likes do |t|
t.references :user
t.references :post
t.timestamps
t.index [:user_id, :post_id], unique: true
end
end
end
「いいね!」はlikes
テーブルにデータをきちんと入力できれば実現されます。
(どうしてそうなるのかは、こちらの記事をご覧くださいませ^^)
Railsで「いいね!」機能を作る - ②「いいね!」のcreateアクション
また、
t.index [:user_id, :post_id], unique: true
の部分で、同じユーザー・同じ投稿への「いいね!」が投稿できないように、DB側で制御をしています。
アソシエーションの定義
posts
、users
、likes
のモデルファイルに、それぞれ下記のようにアソシエーションを記載しました。
class User < ApplicationRecord
# ★1
has_many :posts, dependent: :destroy
# ★2
has_many :likes, dependent: :destroy
has_many :like_posts, through: :likes, source: :post
end
class Post < ApplicationRecord
# ★1
belongs_to :user
# ★2
has_many :likes, dependent: :destroy
has_many :users, through: :likes
end
class Like < ApplicationRecord
belongs_to :post
belongs_to :user
end
User
とPost
の★1と★2の下の2行は、それぞれ
- ★1 ... ユーザーが投稿したpostに関する定義
- ★2 ... ユーザーが「いいね!」したpostに関する定義
です。User
=> Like
=> Post
の流れに、:like_posts
とアソシエーションの別名をつけているのがポイントです。
has_many :like_posts, through: :likes, source: :post
これで、
user = User.first
user.like_posts
↑この形で、ユーザーが「いいね!」した投稿一覧が取得できます。
コントローラーの定義
コントローラーの内容は以下の通りです。「いいね!」(create
)と「いいね!解除」(destroy
)をそれぞれ以下のように定義します。
class LikesController < ApplicationController
def create
@post = Post.find(params[:post])
current_user.like(@post)
end
def destroy
@post = Like.find(params[:id]).post
current_user.unlike(@post)
end
end
コード中にあるlike
とunlike
はそれぞれ、「いいね!」と「いいね解除」を行う、User
のモデルメソッドです。
Userのモデルメソッドの定義(like
とunlike
)
User.rb
には、以下のモデルメソッドを定義します。
class User < ApplicationRecord
has_many :posts, dependent: :destroy
has_many :likes, dependent: :destroy
has_many :like_posts, through: :likes, source: :post
def own?(object)
id == object.user_id
end
def like(post)
likes.find_or_create_by(post: post)
end
def like?(post)
like_posts.include?(post)
end
def unlike(post)
like_posts.delete(post)
end
end
like?
はユーザーがすでにその投稿に「いいね!」しているかを判別するメソッドです。この後ビューで使うので載せています。
また、owm?
も今回の実装には直接関係がないのですが、この後ビューで使用しているので載せています。対象のオブジェクトの作成者を判別するメソッドです。
like_posts.delete(post)
何気に、like_posts
でユーザーが「いいね!」したポスト一覧を取得してdestroyしているのも、ミソかなあと思っています。
ビューの定義(非同期「ではない」実装の場合)
その後、ビューを書いていきます。まずは非同期ではない実装でビューを作成していきます。なお、読みやすさのため、装飾のための要素や機能は省いています。
- @posts.each do |post|
- if logged_in? # ログイン確認
- if current_user.own?(post) # 所有を確認
= link_to post_path(post), method: :delete do
= icon 'far', 'trash-alt' # ゴミ箱アイコン
= link_to edit_post_path(post) do
= icon 'far', 'edit' # 編集アイコン
- else
- if current_user&.like?(post) # すでにいいね!してるか確認
= link_to like_path(current_user.likes.find_by(post: post)), method: :delete do
= icon 'fa', 'heart' # ハート(黒)
- else
= link_to likes_path(post: post), method: :post do
= icon 'far', 'heart' # ハート(白)
logged_in?
はGemで生成されている、ログインしているかどうかを判別するメソッドです。
実は、ここまでの実装で、「いいね!」機能自体はできています。「いいね!」ボタンを押して、画面をリロードすると「いいね!」と「いいね解除」がそれぞれ切り替えられているのがわかります。
▼「いいね!」を押してから、リロード(画面外のボタンを押下)すると、アイコンが切り替わっている
「いいね!」を非同期で実現する
さて、ここからが本題です。これら「いいね!」機能を非同期で実現していきます。
リンクをremote: true
にする
まずはリンクをremote: true
にして、通信を非同期通信にします。
- @posts.each do |post|
- if logged_in?
- # 省略
- else # それぞれ、link_toの後ろにremote: trueを追記
- if current_user&.like?(post)
= link_to like_path(current_user.likes.find_by(post: post)), method: :delete, remote: ture do
= icon 'fa', 'heart'
- else
= link_to likes_path(post: post), method: :post, remote: ture do
= icon 'far', 'heart'
Ajax用のビューファイルを作る - (1)ボタンの移動
上記のビューファイルのうち、さらにlike
とunlike
のボタンについては、それぞれ後に続くAjaxの処理用に、別のパーシャルに分ます。また、Ajaxの処理の目印となるように、id
属性も付与しています。
- @posts.each do |post|
- if logged_in?
- # 省略
- else id="like-button-#{post.id}" # id属性を追記
- if current_user&.like?(post)
= render 'likes/unlike_button', post: post # パーシャルへ移動
- else
= render 'likes/like_button', post: post # 同上
パーシャルの中身はこちらです。
= link_to like_path(current_user.likes.find_by(post: post)), method: :delete, remote: true do
span.c-icon-button= icon 'fa', 'heart', class: 'fa-lg'
= link_to likes_path(post: post), method: :post, remote: true do
span.c-icon-button= icon 'far', 'heart', class: 'fa-lg'
Ajax用のビューファイルを作る - (2).js.erb
ファイルを作る
likes
のcreate
、destroy
アクションに対応した.js.erb
ファイルをそれぞれ作成します。
$("#like-button-<%= @post.id %>").html("<%= j(render 'unlike_button', post: @post) %>")
$("#like-button-<%= @post.id %>").html("<%= j(render 'like_button', post: @post) %>")
.js.erb
ファイルについては、こちらの記事で解説していますので、よろしければご覧ください。
remote: trueでajaxの投稿をPOSTをするよ。
完成!
非常に簡単なステップでしたが、上記の実装で、いいね!機能が非同期で実装できています
意外にあっさり、簡単ですね!
感想...「いいね!」機能もRailsの機能でAjaxを作るのも実は3回目だったので、どちらもスルスルできてよかったです