6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【rails】railsとjsを用いて「いいね機能」を実装してみた

Last updated at Posted at 2020-12-23

今回はrailsとjsでいいね機能を実装していきたいと思います

** また最後におまけでユーザーがいいねした投稿を表示できるような機能も実装していきます**

jsを読み込んだりする説明は割愛!

参考にさせていただいた記事

https://techtechmedia.com/favorite-function-rails/
https://qiita.com/hayabusa3703/items/2b916e652a1dc85bb6e3

完成予想図

スクリーンショット 2020-12-22 21.34.19.png

下準備

ユーザーはたくさんの投稿にいいねをして、投稿もたくさんのユーザーにいいねされるので

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>

自分はこんな感じ

これで以上です。お疲れ様でした。

今までのコードのまとめ

自分のgithub

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?