Rails x Ajaxで「お気に入り」をオンオフできるリストを作成する

# はじめに

- こんな感じのAjaxで「お気に入り」のオンオフできる（ついでにカウントも）リストを作ります。
- `Devise`を使ってログインしている状態を想定しているので、Deviseをある程度知っている人向けです。

![](https://dl.dropboxusercontent.com/s/vt92qx808q75zf3/like_demo_1.gif?dl=0)

## 今回使うライブラリ
- [Devise（ログイン処理。）](https://github.com/plataformatec/devise)
- [Toastr.JS（通知メッセージ）](https://codeseven.github.io/toastr/)
- [CounterCulture（お気に入りカウント）](https://github.com/magnusvk/counter_culture)
- [FontAwesomeRails（フリーアイコン）](https://github.com/bokmann/font-awesome-rails)
- DeviseとCounterCulture、FontAwesomeRailsはGemなので、READMEに従って入れましょう。
- Toastrだけは自分でJSとCSSファイルをダウンロードしてきて、`vendor/assets/toastr`ディレクトリ配下に配置しましょう。パイプラインでの設定を忘れずに。

## 必要な予備知識
1. RailsのAjax通信。
2. BootStrap4（ビューの整形に使っているだけなので、詳しくなくていい）
3. Slimテンプレート（2と同様）
4. 使われているライブラリの知識

## 環境
```
$ ruby -v
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]

$ rails -v
Rails 5.2.1
```

# 実装
## モデル
- 今回は`User`と`Dog`、`Like`モデルを使います。
- `User`モデルは`Devise`で作成します。
- `Like`を中間テーブルとして、**`User`と`Dog`を多対多の関連づけすることで、いいねリレーションを構築します。**
- `seed-fu`とかを使って、前もってページネーション用のインスタンスをDBに作っておきましょう。

```rb
# マイグレーション
class CreateDogs < ActiveRecord::Migration[5.2]
  def change
    create_table :dogs do |t|
      t.string :name, null: false, default: ''
      t.integer :receive_likes_count, null: false, default: 0
    end
  end
end

class CreateLikes < ActiveRecord::Migration[5.2]
  def change
    create_table :likes do |t|
      t.references :user
      t.references :dog
    end
  end
end
```

```rb
class User < ApplicationRecord
  has_many :likes, dependent: :destroy
end

class Dog < ApplicationRecord
  has_many :receive_likes, dependent: :destroy, class_name: 'Like'

  # 特定のユーザーからお気に入りされているか
  def liked_by?(user)
    user.likes.exists?(dog_id: id)
  end

  # Ajax実行時に、DOMエレメントを特定するID
  def id_attribute
    "dog-#{id}"
  end
end

class Like < ApplicationRecord
  belongs_to :user
  belongs_to :dog
  # counter culture設定
  counter_culture :dog, column_name: 'receive_likes_count'
end
```

## コントローラー
- 今回のページを`sample#index`アクションとします。
- 「いいね」しても順番が表示の順番が変わらないよう、`order`を設定しておきます。

```rb
class SampleController < ApplicationController
  def index
    @dogs = Dog.all.order(created_at: :asc) 
  end
end
```

- また、いいねをAjaxでオンオフするためのAPIコントローラー`api/like_controller.rb`も追加します。
- このコントローラー呼び出し時に実行されるJSファイルは後ほど記載します。

```rb
class Api::LikeController < ApplicationController
  before_action :setup_dog!
  before_action :setup_like!

  def create
    @like.save
  end

  def destroy
    @like.destroy
  end

  private

  def setup_dog!
    @dog = Dog.find(params[:dog_id])
  end

  def setup_like!
    # create時はまだ存在していないのでinitializeされ、
    # destroy時は既に存在しているのでfindされる。
    @like = current_user
              .likes
              .find_or_initialize_by(dog_id: @dog.id)
  end
end
```

- ルーティングはこんな感じ。

```rb
namespace :api do
  post 'like/:dog_id' => 'like#create', as: :like
  delete 'like/:dog_id' => 'like#destroy', as: :unlike
end
```

## ヘルパー
- いいねをオンオフするボタンをレンダーするヘルパーメソッドを作成する。
- これにより、ログイン中のユーザーが特定の`Dog`モデルをお気に入りしたかどうかを判定して、アイコンとリンクを出し分けることができる。

```rb
module MyHelper
  def toggle_like_button(dog)
    # ログインしていない場合
    return fa_icon 'heart-o', text: dog.receive_likes_count if current_user.blank?

    liked = dog.liked_by?(current_user)
    icon = fa_icon (liked ? 'heart' : 'heart-o'), text: dog.reload.receive_likes_count
    if liked
      # お気に入りを外すボタン
      link_to icon, api_unlike_path(dog_id: dog.id), remote: true, method: :delete
    else
      # お気に入りするボタン
      link_to icon, api_like_path(dog_id: dog.id), remote: true, method: :post
    end
  end
end
```

## ビュー
- 今回用意するビューは、**大元のページ用**と、**Dogのパーシャルビュー**の2つです。

```
# ページ
app/views/sample/
├── _dog.html.slim
└── index.html.slim
```

```slim
/ index.html.slim
.container.my-5
  .row
    .col-md-6.offset-md-3
      #project-list(style='overflow-y: scroll; height: 80vh;')
        = render @dogs
```

```slim
/ _dog.html.slim
.card.card-body.mb-3(id="#{dog.id_attribute}")
  / この.like要素をAjaxで書き換える
  .like
    = toggle_like_button(dog)
  h3.card-title = dog.name
```

## JSファイル
- 最後にlikeコントローラー呼び出し時に呼び出されるJSファイルを追加します。

```
app/views/api/like/
├── create.js.erb
└── destroy.js.erb
```

```js
// create.js.erb

// toastrの通知処理
toastr.success("<%= "#{@like.dog.name}をお気に入りに追加しました" %>")
// 特定のLIKEボタンのDOMを入れ替える。
$("<%= "##{@dog.id_attribute} .like" %>").html("<%= j toggle_like_badge(@dog) %>") 
```

```js
// destroy.js.erb

toastr.error("<%= "#{@dog.name}をお気に入りから削除しました" %>")
$("<%= "##{@dog.id_attribute} .like" %>").html("<%= j toggle_like_badge(@dog) %>")
```

## おわりに
- かけあしでしたが、ひとまずこれで「お気に入り処理」ができるようになりました。
- いろんな便利なライブラリがあるっていいですね。