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

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

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

### 本

[改訂4版　基礎Ruby on Rails 基礎シリーズ](https://www.amazon.co.jp/%E6%94%B9%E8%A8%824%E7%89%88-%E5%9F%BA%E7%A4%8ERuby-Rails-%E5%9F%BA%E7%A4%8E%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA-%E9%BB%92%E7%94%B0%E5%8A%AA-ebook/dp/B07GXB2ZSW/ref=sr_1_2?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=rails+%E5%9F%BA%E7%A4%8E&qid=1609224169&sr=8-2)

### 記事
[【Rails】いいね機能完全版！同期いいね、いいね数の表示、非同期いいね、アイコン表示、それぞれの実装方法についてまとめて解説](https://techtechmedia.com/favorite-function-rails/)

[js.erbを使って非同期通信（いいね機能を10分で実装）](https://qiita.com/params_bird/items/0fada2c5b322bb28ad98)

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

![465205b7ae2f8d84bb28a165cdf7a674.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727548/01249553-4920-b693-af13-3c48848767a0.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を付与
    ４. ｊｓ．erbファイルを作成

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

## 1.同期通信いいねの実装
### 1. 中間テーブルモデルの作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727548/46c5dcb6-2dd2-e453-fece-444927017e5d.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をコンソールで確認するとログインしているユーザーの情報が格納されています。
<br>

加えて、ユーザーがいいねボタンを解除できるか判断するためのメソッドも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
（以下省略）
```
<br>
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. いいねボタンを作成

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

```erb: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コントローラー内で作成します。

```erb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727548/3b38fc92-fe5a-9ea0-3d85-8bc9bd4ffc4c.gif)


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

### 1. link_toの引数にremote: trueを付与
いいねボタン用の部分テンプレートを修正します。

```erb: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を付与
```erb:app/views/photos/showl.html.erb
（省略）
      <div id="vote-box"> # ←idを付与
        <%= render partial: 'shared/votes', locals: { photo: @photo } %>
      </div>
（以下省略）
```
この後作成する`ｊｓ．erb`ファイル内で、いいねボタン用部分テンプレートの外枠ブロック（div）のidをもとに、この外枠ブロックを取得し、ブロック内の部分テンプレートを再読み込みさせます。"vote-box"というidを付与しました。

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

### ４. ｊｓ．erbファイルを作成
```erb:app/views/photos/vote.js.erb
document.getElementById("vote-box").innerHTML = '<%= escape_javascript(render partial: 'shared/votes', locals: { photo: @photo }) %>'
```
このコードで、いいねボタン用の部分テンプレートを再読込します。

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

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

↓挙動確認（いいねボタンを押しても、左上の再読み込みボタンは変わりません）
![465205b7ae2f8d84bb28a165cdf7a674.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727548/c9ad4d40-643e-b5c6-c5c4-e4e2356165d2.gif)

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

```erb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727548/edf45cd7-824d-33ee-fa38-5f3a72318666.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](https://www.youtube.com/watch?v=vqA7p44YUTY&t=94s)
[【Font Awesome】基本的な使い方（導入〜アニメーション）初心者向け Font Awesome tutorial](https://www.youtube.com/watch?v=B6AU1jj_WLo&t=71s)


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