RailsでAjaxで「いいね！」機能を実装する。

## 何をしたか
Railsの課題を実施しています。その中で「いいね！」機能をAjaxで作成しましょうというタスクがありました。
実は以前、非同期通信**ではない**「いいね！」機能は作成したことがありました。

▼その時の記録はこちら
[Railsで「いいね！」機能を作る - ①アソシエーションに別名をつける](https://qiita.com/tanutanu/items/2a27f1e503c4cbf30928)
[Railsで「いいね！」機能を作る - ②「いいね！」のcreateアクション](https://qiita.com/tanutanu/items/bff95ce3a91dab1eb2c6)
[Railsで「いいね！」機能を作る - ③「いいね！」を解除できるようにする](https://qiita.com/tanutanu/items/89cd52ed57b487fed84d)

また、別の記事で**「いつかAjaxで”いいね！”機能を作ってみたい」**と言いつつ、作っていなかったので、作成の手順をノートにまとめたいと思います。

なお、ここではメモ程度に実装手順を紹介しています。実際に詳しい実装の手順は↑上記の記事をご覧ください。

また、実装環境は以下の通りです。

- `Rails 5.2.3`
- `Ruby 2.6.0`

## 仕様確認・定義する
「いいね！」機能はアソシエーションの定義に若干ひねりが必要です。今回もまずはDB構造と仕様を確認します。その結果、DB構造と仕様は下記の通りでした。

[![Image from Gyazo](https://i.gyazo.com/e3186afd87a502c236af4620c0570774.png)](https://gyazo.com/e3186afd87a502c236af4620c0570774)

※`images`テーブルは画面外にあります。
※他にもあるテーブルの中の一部を表示しています。

このうち、**「自分の投稿にいいね！できない」**というのは、下記のように、自分の投稿の時にはビューに「いいね！」ボタンを表示しない、という形で実現しています。

▼自分の投稿の時（編集・削除ボタンを表示）
[![Image from Gyazo](https://i.gyazo.com/1d7e52dff0242bc3e798455857e1ec9f.png)](https://gyazo.com/1d7e52dff0242bc3e798455857e1ec9f)

▼自分以外の投稿の時（いいね！ボタンを表示）
[![Image from Gyazo](https://i.gyazo.com/2e541d5d6381fe3a4aa55c4e64b76ba6.png)](https://gyazo.com/2e541d5d6381fe3a4aa55c4e64b76ba6)

また、**`like_posts`**と赤文字で書いてあるのは、この後記載するアソシエーションの別名です。
「アソシエーションの別名って？？？」という方は、こちらの記事に詳しく説明してあります。

[Railsで「いいね！」機能を作る - ①アソシエーションに別名をつける](https://qiita.com/tanutanu/items/2a27f1e503c4cbf30928)

## マイグレーションの作成
では、ここから実装していきます。まずは、マイグレーションファイルを作成します。
`users`テーブルと`posts`テーブルはそれぞれ作成済という前提です。
`likes`テーブルは下記のように作成します。

```Ruby:db/migrate/XXXXXXXXXX_create_likes.rb
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アクション](https://qiita.com/tanutanu/items/bff95ce3a91dab1eb2c6)

また、

```Ruby
t.index [:user_id, :post_id], unique: true
```
の部分で、同じユーザー・同じ投稿への「いいね！」が投稿できないように、DB側で制御をしています。

## アソシエーションの定義

`posts`、`users`、`likes`のモデルファイルに、それぞれ下記のようにアソシエーションを記載しました。

```Ruby
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`の★１と★２の下の2行は、それぞれ

- **★１ ... ユーザーが投稿したpostに関する定義**
- **★2 ... ユーザーが「いいね！」したpostに関する定義**

です。`User` => `Like` => `Post` の流れに、`:like_posts`とアソシエーションの別名をつけているのがポイントです。

```Ruby
has_many :like_posts, through: :likes, source: :post
```

これで、

```Ruby
user = User.first
user.like_posts
```

↑この形で、ユーザーが「いいね！」した投稿一覧が取得できます。

## コントローラーの定義

コントローラーの内容は以下の通りです。「いいね！」（`create`）と「いいね！解除」（`destroy`）をそれぞれ以下のように定義します。

```Ruby:likes_controller.rb
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`には、以下のモデルメソッドを定義します。

```Ruby:models/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?`も今回の実装には直接関係がないのですが、この後ビューで使用しているので載せています。対象のオブジェクトの作成者を判別するメソッドです。

```Ruby
like_posts.delete(post)
```

何気に、`like_posts`でユーザーが「いいね!」したポスト一覧を取得してdestroyしているのも、ミソかなあと思っています。

## ビューの定義（非同期「ではない」実装の場合）

その後、ビューを書いていきます。まずは**非同期ではない**実装でビューを作成していきます。なお、読みやすさのため、装飾のための要素や機能は省いています。

```slim
- @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で生成されている、ログインしているかどうかを判別するメソッドです。

実は、ここまでの実装で、「いいね！」機能自体はできています。「いいね！」ボタンを押して、画面をリロードすると「いいね！」と「いいね解除」がそれぞれ切り替えられているのがわかります。

▼「いいね！」を押してから、リロード（画面外のボタンを押下）すると、アイコンが切り替わっている
[![Image from Gyazo](https://i.gyazo.com/101f94b4a7608139ba95723c719d2b2b.gif)](https://gyazo.com/101f94b4a7608139ba95723c719d2b2b)

## 「いいね！」を非同期で実現する
さて、ここからが本題です。これら「いいね！」機能を非同期で実現していきます。

### リンクを`remote: true`にする

まずはリンクを`remote: true`にして、通信を非同期通信にします。

```slim
- @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`属性も付与しています。

```slim
- @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 # 同上
```

パーシャルの中身はこちらです。

```slim:views/likes/_unlike_button.html.slim
= 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'
```

```slim:views/likes/_like_button.html.slim
= 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`ファイルをそれぞれ作成します。

```js:/views/likes/create.js.erb
$("#like-button-<%= @post.id %>").html("<%= j(render 'unlike_button', post: @post) %>")
```

```js:/views/likes/destroy.js.erb
$("#like-button-<%= @post.id %>").html("<%= j(render 'like_button', post: @post) %>")
```

`.js.erb`ファイルについては、こちらの記事で解説していますので、よろしければご覧ください。

[remote: trueでajaxの投稿をPOSTをするよ。](https://qiita.com/tanutanu/items/61e837c1cf1efc618779)

## 完成！

非常に簡単なステップでしたが、上記の実装で、いいね！機能が非同期で実装できています:relaxed:
意外にあっさり、簡単ですね！

[![Image from Gyazo](https://i.gyazo.com/f0e9c11240509a1156184c8da74a5fd0.gif)](https://gyazo.com/f0e9c11240509a1156184c8da74a5fd0)

感想...「いいね！」機能もRailsの機能でAjaxを作るのも実は３回目だったので、どちらもスルスルできてよかったです:relaxed: