7
8

More than 3 years have passed since last update.

【Rails、JavaScript】いいね機能を実装(同期・非同期通信)

Last updated at Posted at 2020-12-29

概要

いいね機能の実装方法についてまとめます。

参照

同期通信のいいね機能実装は以下の本を参考に、その後の非同期通信化は以下の記事を参考にして行いました。
ありがとうございます。

改訂4版 基礎Ruby on Rails 基礎シリーズ

記事

【Rails】いいね機能完全版!同期いいね、いいね数の表示、非同期いいね、アイコン表示、それぞれの実装方法についてまとめて解説

js.erbを使って非同期通信(いいね機能を10分で実装)

完成イメージ

今回は、写真投稿アプリを題材にいいね機能を実装する方法をまとめます。

465205b7ae2f8d84bb28a165cdf7a674.gif

開発環境

macOS Catalina 10.15.7
ruby 2.6.5
Rails 6.0.3.4

実装の流れ

1.同期通信いいねの実装

1. 中間テーブルモデルの作成
2. モデルの関連付け
3. いいねルールをメソッドに設定
4. ルーティングを設定
5. いいねボタンを作成
6. いいねアクションをコントローラーに設定

2.いいね機能の非同期通信化

1. link_toの引数にremote: trueを付与
2. いいねアクション後の処理をredirect_toからrenderに変更
3. いいねボタン用部分テンプレートの外枠ブロックにidを付与
4. js.erbファイルを作成

今回のコード

コードは必要な部分のみ記載します。
CSSコードなどは省略してあります。

1.同期通信いいねの実装

1. 中間テーブルモデルの作成

image.png

いいねをしたuserのidと、いいねをされたphotoのidを保存するためのテーブルとして、votesという中間テーブル(Voteモデル)を作成します。

% rails g model vote
db/migrate/□□□□□□□□□□□□□□_create_votes.rb
class CreateVotes < ActiveRecord::Migration[6.0]
  def change
    create_table :votes do |t|
      t.references :photo, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
      t.timestamps
    end
  end
end
% rails db:migrate

2. モデルの関連付け

モデル間のアソシエーションを以下のように設定します。

app/model/vote.rb
class Vote < ApplicationRecord
  belongs_to :user
  belongs_to :photo
(以下省略)
app/models/user.rb
class User < ApplicationRecord
  has_many :votes, dependent: :destroy
  has_many :voted_photos, through: :votes, source: :photo
(以下省略)

dependent: :destroyとすることで、userが削除されると、そのuserに関連したvotesテーブルのレコードも削除されます。

has_many :photosとすると、votesを介した関連性がわかりにくくなるため、かわりに:voted_photosという名前を用いています。その後、:voted_photosは:photoモデルの:photosだと設定するためにsourceオプションを使っています(説明がわかりづらい)。

app/models/photo.rb
class User < ApplicationRecord
  has_many :votes, dependent: :destroy
  has_many :voters, through: :votes, source: :user
(以下省略)

こちらも同様に、:userのかわりに:votersという名前を用いています。

3. いいねルールをメソッドに設定

ユーザーがいいねボタンを押せるか判断するためのメソッドをUserモデルに作成します。
具体的には以下のルールを設定します。
①自分が投稿した写真にはいいねできない
②1つの写真には1回しかいいねできない

app/models/user.rb
class User < ApplicationRecord
(省略)
  def deletable_vote?(photo)
    photo && photo.user != self && !votes.exists?(photo_id: photo.id)
  end
(以下省略)

『photoが存在する』かつ『photoのuserはログインしているuserではない』かつ『ログインuserはこのphotoにいいねしていない』が真であればtrueが返されます。

selfをコンソールで確認するとログインしているユーザーの情報が格納されています。

加えて、ユーザーがいいねボタンを解除できるか判断するためのメソッドもUserモデルに作成します。
『photoが存在する』かつ『photoのuserはログインしているuserではない』かつ『ログインuserはこのphotoにいいねしている』が真であればtrueが返されます。

app/models/user.rb
class User < ApplicationRecord
(省略)
  def votable_for?(photo) # ←いいなボタンを押せるか判断
    photo && photo.user != self && !votes.exists?(photo_id: photo.id)
  end

  def deletable_for?(photo) # ←いいねボタンを解除できるか判断
    photo && photo.user != self && votes.exists?(photo_id: photo.id)
  end
(以下省略)



Voteモデルにvalidateメソッドを設定して、userが上記ルールに合わない場合はいいねが保存されないようにします。

app/models.vote.rb
class Vote < ApplicationRecord
  belongs_to :user
  belongs_to :photo

  validate do
    unless user && user.votable_for?(photo)
      errors.add(:base, :invalid)
    end
  end
end

4. ルーティングを設定

config/routes.rb
Rails.application.routes.draw do
(省略)
  resources :photos do
(省略)
    patch "like", "unlike", on: :member
    get "voted", on: :collection
(以下省略)
  end

likeアクション:いいねボタンをクリックするとvotesテーブルにレコードが保存される
unlikeアクション:いいねボタンをクリックするとvotesテーブルからレコードが削除される
votedアクション:自分がいいねした写真の一覧を表示する(今回は説明しません)

5. いいねボタンを作成

いいねボタン用の部分テンプレートを作成します。
上記で作成したいいねルールをもとにボタンをかえて表示させます("いいね"または"いいねを解除")

app/views/shared/_votes.htmnl.erb
  <div>
    <% if current_user && current_user.votable_for?(photo) %>
      <%= link_to "いいね" like_photo_path(photo), method: :patch %>
    <% elsif current_user && current_user.deletable_for?(photo) %>
      <%= link_to "いいねを解除", unlike_photo_path(photo), method: :patch %>
    <% end %>
  </div>

今回は写真詳細ページで、いいねボタン用の部分テンプレートをrenderで読み込みます。こうすることで、写真詳細ページにいいねボタンが設置されます。photo: @photoとすることで部分テンプレート内のローカル変数photoにインスタンス変数@photoをわたします。@photoはこの後photosコントローラー内で作成します。

app/views/photos/showl.html.erb
(省略)
      <div>
        <%= render partial: 'shared/votes', locals: { photo: @photo } %>
      </div>
(以下省略)

6. いいねアクションをコントローラーに設定

app/controllers/photos_controller.rb
class PhotosController < ApplicationController
  before_action :set_photo, only: [(省略), :like, :unlike]
(省略)
  # いいね
  def like
    current_user.voted_photos << @photo
    redirect_to photo_path
  end

  # いいね削除
  def unlike
    current_user.voted_photos.destroy(@photo)
    redirect_to photo_path
  end
  private
(省略)
  def set_photo
    @photo = Photo.find(params[:id])
  end
(以下省略)

current_user.voted_photos << @photo
とすることで、current_userのuser_idと@photoのphoto_idがvotesテーブルに保存されます。

また

current_user.voted_photos.destroy(@photo)
とすることで、current_userのuser_idと@photoのphoto_idがvotesテーブルから削除されます。

どちらも、usersテーブルとphotosテーブルのレコードには影響を与えません。

以上で、同期通信のいいねボタンが実装できました。

↓挙動確認(いいねボタンを押すたびに、左上の再読み込みボタンがXになっているのが確認できます)
08e86be3b1268fad5ae9fa7ef3b91dcb.gif

2.いいね機能の非同期通信化

ここからは、いいね機能を非同期通信化します。

1. link_toの引数にremote: trueを付与

いいねボタン用の部分テンプレートを修正します。

app/views/shared/_votes.htmnl.erb
  <div>
    <% if current_user && current_user.votable_for?(photo) %>
      <%= link_to "いいね" like_photo_path(photo), method: :patch, remote: true %>
    <% elsif current_user && current_user.deletable_for?(photo) %>
      <%= link_to "いいねを解除", unlike_photo_path(photo), method: :patch, remote: true %>
    <% end %>
  </div>

"いいね"と"いいねを解除"のlink_toの引数にremote: trueを付与します。
これで非同期通信機能を実装することができます。

2. いいねアクション後の処理をredirect_toからrenderに変更

app/controllers/photos_controller.rb
(省略)
  # いいね
  def like
    current_user.voted_photos << @photo
    render 'vote.js.erb' # renderメソッドを追加
    # redirect_to photo_path, notice: "いいねしました。←削除"
  end

  # いいね削除
  def unlike
    current_user.voted_photos.destroy(@photo)
    render 'vote.js.erb' # renderを追加
    # redirect_to photo_path, notice: "削除しました。←削除"
  end
(以下省略)

likeアクションまたはunlikeアクションの処理後に、render 'vote.js.erb'とすることで、vote.js.erbファイルが実行されます。vote.js.erbはこの後作成します。

3. いいねボタン用部分テンプレートの外枠ブロックにidを付与

app/views/photos/showl.html.erb
(省略)
      <div id="vote-box"> # ←idを付与
        <%= render partial: 'shared/votes', locals: { photo: @photo } %>
      </div>
(以下省略)

この後作成するjs.erbファイル内で、いいねボタン用部分テンプレートの外枠ブロック(div)のidをもとに、この外枠ブロックを取得し、ブロック内の部分テンプレートを再読み込みさせます。"vote-box"というidを付与しました。

今回は、写真詳細画面(1投稿分のみ表示)にいいねボタンを設置したため、部分テンプレート外枠のidは"vote-box"のみのひとつとしています。もし写真一覧ページなどで各写真にいいねボタンを設置する場合は、各いいねボタンの外枠ブロックごとに異なるidを付与する必要があります。その場合は、コントローラー内で各いいねボタンの外枠ブロックごとに異なるid名が作成されるようにするなど工夫が必要です(例えば、@id_name = "vote-box-#{@photo.id}"など。詳細な説明は省略します)。

4. js.erbファイルを作成

app/views/photos/vote.js.erb
document.getElementById("vote-box").innerHTML = '<%= escape_javascript(render partial: 'shared/votes', locals: { photo: @photo }) %>'

このコードで、いいねボタン用の部分テンプレートを再読込します。

もし、"いいね"状態のボタンをクリックした場合は、"いいね解除"が表示されるようになります。
また、"いいね解除"状態のボタンをクリックした場合は、"いいね"が表示されるようになります。

以上で、非同期通信のいいねボタンが実装できました。

↓挙動確認(いいねボタンを押しても、左上の再読み込みボタンは変わりません)
465205b7ae2f8d84bb28a165cdf7a674.gif

おわりに

もし、いいねボタンをアイコン化したい場合は、以下のようにFontAwsomeを利用することでできます。

app/views/shared/_votes.htmnl.erb
<div>
    <% if current_user && current_user.votable_for?(photo) %>
      <%= link_to like_photo_path(photo), method: :patch, remote: true do %>
        <i class="far fa-heart"></i>
      <% end %>
      <span><%= photo.votes.count %></span> # ←いいねの数も表示させています
    <% elsif current_user && current_user.deletable_vote?(photo) %>
      <%= link_to unlike_photo_path(photo), method: :patch, remote: true do %>
        <i class="fas fa-heart"></i>
      <% end %>
      <span><%= photo.votes.count %></span> # ←いいねの数も表示させています
    <% end %>
  </div>

↓いいねボタンをハートマークにしてみました。
f354fe989d3f788243cf1394f68fb05d.gif

FontAwsomeの使い方については以下の動画などをご参照ください。
FontAwsomeのgemもあるのですが、gemだとうまく導入できなかったため、FontAwsomeのホームページからKit's Codeをapplication.html.erbにコピペしてFontAwsomeを導入しました(以下動画参照)。

【Ruby on Rails】Font Awesomeをrailsのボタンに導入する Adding Font Awesome to Ruby on Rails
【Font Awesome】基本的な使い方(導入〜アニメーション)初心者向け Font Awesome tutorial

間違いや改善点などありましたらご指摘お願いいたします。

7
8
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
7
8