今回はrailsとjsでいいね機能を実装していきたいと思います
** また最後におまけでユーザーがいいねした投稿を表示できるような機能も実装していきます**
jsを読み込んだりする説明は割愛!
参考にさせていただいた記事
https://techtechmedia.com/favorite-function-rails/
https://qiita.com/hayabusa3703/items/2b916e652a1dc85bb6e3
完成予想図
下準備
ユーザーはたくさんの投稿にいいねをして、投稿もたくさんのユーザーにいいねされるので
likesテーブルを中間テーブルにした、ユーザと投稿の多対多のテーブル構造
rails g model like
マイグレーションファイル
class CreateLikes < ActiveRecord::Migration[5.0]
def change
create_table :likes do |t|
t.integer :user_id
t.integer :drink_id
t.timestamps
end
end
end
rails g controller likes
アソシエーション
like.rb
class Like < ApplicationRecord
belongs_to :user
belongs_to :drink, counter_cache: :likes_count
end
・counter_cahce: :likes_countはリレーションされているlikeの数の値をリレーション先のlikes_countというカラムの値に入れますよっていう意味です。なのでlikes_countカラムをstoriesテーブルに追加しましょう。(rails g migration AddLikes_countToStories likes_count:integerをターミナルで実行すればオッケーです。)
この文章の参照元
drink.rb
class Drink < ApplicationRecord
has_many :likes
has_many :liking_users, through: :likes, source: :user
end
liking_usersモデルは無いので、likesテーブルを中間テーブルにして、userモデルとアソシエーションを汲みますよーってことをrailsに伝えてます
has_manyはbelongs_toはアソシエーションを組むのが本質ではなくて、メソッドを作るメソッド。
つまり,@drink.liking_userとかやったら、その投稿にいいねしたユーザー一覧を取得できるメソッドができるし、アソシエーションも組める
user.rb
has_many :likes
has_many :like_drinks, through: :likes, source: :drink
これも、user.like_drinksとかやったら、そのユーザーがいいねした投稿一覧が取得できる
これは、インスタ、Twitterによくある、そのユーザーがいいねした投稿を表示する時に便利
has_manyはbelongs_toはアソシエーションを組むのが本質ではなくて、メソッドを作るメソッド。
これを覚えて帰りましょう。
#いいねボタンの記述
drinks/index.html.erb
こちらは投稿一覧のページになります
<%if @drinks%>
<% @drinks.each do |drink|%>
<li class='list'>
<%= link_to drink_path(drink.id) do %>
<%= link_to user_path(drink.user.id) do%>
<div class="user-info-timeline">
<%=image_tag drink.user.image.variant(resize: '60x60'),class: "user-img-timeline" if drink.user.image.attached?%>
<div class="username-timeline">
<%= drink.user.nickname %>
</div>
</div>
<% end %>
<div class='item-img-content'>
<%= image_tag drink.image , class: "item-img" if drink.image.attached? %>
<%# if drink.trade%>
<%# end %>
</div>
<div class='item-info'>
<h3 class='item-name'>
<%= drink.name %>
</h3>
<div class='item-price'>
<span><%= drink.price %>円<br>(税込み)</span>
<div class='star-btn'>
<%# image_tag "star.png", class:"star-icon" %>
<span class='star-count'>0</span>
</div>
</div>
<div class='item-explain'>
<%= drink.explain%>
</div>
<div class='item-tag'>
<% drink.tags.each do |tag| %>
#<%=tag.tag_name%>
<%end%>
</div>
<%= render "likes/like",drink: drink%>
</div>
<% end %>
</li>
<%end%>
<%= render "likes/like",drink: drink%>
に注目して欲しいです!
まずは可読性を高めるために
画像のいいねボタンを部分テンプレートで切り出しています、
そして、,drink: drinkの部分ですが、
<% @drinks.each do |drink|%>
のeach文内のブロック変数を、likes/likeにも適用するために変数を受け渡しています。
ブロック変数とは(分かる人は飛ばして)
ブロック変数とは、each文やらtimes文,form_withとか、そのメソッド内だけで使える変数です。
つまり、eachだったらeachから endまでの範囲無で使える変数
@drinksにはいろんな情報が、配列として入っていますが、|drink|
とすることで、配列の中の一つ一つの情報がdrinkに入っていって、@drinksにある配列の数だけ表示します
likes/_like.html.erb
パーシャル(部分テンプレート)であることを分かりやすくするために慣習的にファイル名を_likeとしてます。
ただ
<%= render "likes/like",drink: drink%>
で呼び出す時はアンダーバーはいりません
<div class="like" id="like-link-<%= drink.id %>">
<% if current_user.likes.find_by(drink_id: drink.id) %>
<%= link_to unlike_path(drink.id), method: :delete, remote: true do %>
<div class = "iine__button">❤️<%= drink.likes.count %></div>
<% end %>
<% else %>
<%= link_to like_path(drink.id), method: :post, remote: true do %>
<div class = "iine__button">♡️<%= drink.likes.count %></div>
<% end %>
<% end %>
</div>
id="like-link-<%= drink.id %>"
がミソ。
jsで非同期で画面を切り替えたいので、idを取得できるように、投稿ごとにidを区別するために
このように記述しましょう。
<%= link_to unlike_path(drink.id), method: :delete, remote: true do %>
, remote: true
と記述することにより、
リンクを押した時にajaxが発火するので非同期で通信が行われます。
いいねボタンを押したらいいねがすでについてれば、unlike_pathそうじゃなければlike_pathに飛びます
それぞれのpathをまだ定義してないので、このままじゃルーティングエラーになってしまうので
routes.rb
post 'like/:drink_id' ,to: 'likes#like', as: 'like'
delete 'like/:drink_id',to: 'likes#unlike', as: 'unlike'
と記述しましょう
as: 'like' とすることにより本来ならlikes_like_path(drink.id)とパス指定をしなきゃいけないのですが、
like_path(drink.id)でlikes#likeにpostリクエストを送ることができます
これで、リンクを踏んでリクエストを送ることができたので、次はコントローラーをみていきましょう
likes_controller
class LikesController < ApplicationController
include SessionsHelper
before_action :set_variables
def like
like = current_user.likes.new(drink_id: @drink.id)
#redirect_to drinks_path
# jsを用いるので画面遷移は行わない
#binding.pry
like.save
end
def unlike
like = current_user.likes.find_by(drink_id: @drink.id).destroy
#binding.pry
end
private
def set_variables
@drink = Drink.find(params[:drink_id])
@id_name = "#like-link-#{@drink.id}"
end
end
remote: trueのリンクからlike,unlikeアクションが呼び出されるので、
デフォルトの遷移先はilke.js.erb,unlike.js.erbとそれぞれなります。
「⚠︎ @id_name = "#like-link-#{@drink.id}"
とControllerにViewの処理を書くのは、MVCパターン的にあまりよろしくないと思いますね。」
とご指摘をいただいたので、あまりよく無いですが、機能的には問題無いので一旦次いきます。
likes/like.js.erb
$("<%= @id_name %>").html('<%= escape_javascript(render("likes/like", drink: @drink )) %>');
/likes/unlike.js.erb
$("<%= @id_name %>").html('<%= escape_javascript(render("likes/like", drink: @drink )) %>');
likes/_like.html.erbにまた戻ります
この時にまた
drink: @drink
と書いて_like.html.erbに変数を受け渡してあげましょう
この@drinkは
likes_controllerの
private
def set_variables
@drink = Drink.find(params[:drink_id])
@id_name = "#like-link-#{@drink.id}"
end
の@drinkです。
以上で実装終了です。お疲れ様でした。
おまけ、ユーザーがいいねした投稿を表紙
users/show.html.erb
<%= link_to "#{@user.nickname}がいいねした投稿",user_likes_path(@user.id)%>
こんな感じのリンクを作成
@userhはusers#showで@user = User.find(params[:id])
とかよくある感じで定義してます
user_like_pathはまだ定義してないので
routes.rb
get 'user/likes/:id', to: 'users#likes',as: 'user_likes'
resources :users do
member do
get :following,:followers
# memberメソッドを使うと
# ユーザーidが含まれてるURlを扱うようになる
end
end
resources :userとかみんなやると思うので、resourcesの上に get 'user/likes/:id', to: 'users#likes',as: 'user_likes'
を書きましょう
これで、 リンクを踏んだらusers#likesにGETリクエストを飛ばすことができます
users_controller
def likes
@user = User.find(params[:id])
@drinks = @user.like_drinks.paginate(page: params[:page],per_page: 10).order("created_at DESC")
end
こんな感じで実装しましょう
.paginate(page: params[:page],per_page: 10)
はページネーション をまだ取り入れてなければ書かなくて大丈夫です。
.like_drinksメソッドは
has_many :like_drinks, through: :likes, source: :drink
とuser.rbで書いたので、ユーザーがいいねした投稿一覧を取得できます。
デフォルトで、users/likes.html.erbにリダイレクトされるので、そのビューも用意しましょう
users/likes.html.erb
<div class="user-profile">
<h2 class="user-profile-name"><%= current_user.nickname %></h2>
<h2><%= image_tag @user.image.variant(resize: '100x100'),class: 'user-img' if @user.image.attached? %></h2>
<div class="user-like-post">
<%= link_to "#{@user.nickname}がいいねした投稿",user_likes_path(@user.id)%>
</div>
<div class="user-edit">
<% if current_user?(@user) %>
<%= link_to "プロフィールを編集",edit_user_path(@user)%>
<% end %>
</div>
<% unless current_user?(@user) %>
<div id="follow_form">
<% if current_user.following?(@user) %>
<%= render 'unfollow' %>
<% else %>
<%= render 'follow' %>
<% end %>
</div>
<% end %>
</div>
<% @user ||= current_user %>
<div class="stats">
<a href="<%= following_user_path(@user) %>">
<strong id="following" class="stat">
<%= @user.following.count %>
</strong>
following
</a>
<a href="<%= followers_user_path(@user) %>">
<strong id="followers" class="stat">
<%= @user.followers.count %>
</strong>
followers
</a>
</div>
<div class='main'>
<%# 商品一覧 %>
<div class='item-contents'>
<h2 class='title'><%= @user.nickname%>の投稿</h2>
<%= will_paginate @drinks%>
<ul class='item-lists'>
<%# 商品のインスタンス変数になにか入っている場合、中身のすべてを展開できるようにしましょう %>
<%if @drinks%>
<% @drinks.each do |drink|%>
<li class='list'>
<%= link_to drink_path(drink.id) do %>
<div class='item-img-content'>
<%= image_tag drink.image , class: "item-img" if drink.image.attached? %>
<%# if drink.trade%>
<%# end %>
</div>
<div class='item-info'>
<h3 class='item-name'>
<%= drink.name %>
</h3>
<div class='item-price'>
<span><%= drink.price %>円<br>(税込み)</span>
<div class='star-btn'>
<%# image_tag "star.png", class:"star-icon" %>
<span class='star-count'>0</span>
</div>
</div>
<div class="item-explain">
<%= drink.explain%>
</div>
</div>
<% end %>
</li>
<%end%>
</ul>
<%= will_paginate @drinks%>
</div>
<%end%>
</div>
自分はこんな感じ
これで以上です。お疲れ様でした。