【Rails】いいね機能の実装(.js.erbを用いない方法)【プログラミング学習165日目】

# はじめに
今回は、ポートフォリオにいいね機能を実装してみて色々と苦労したので備忘録も兼ねて記事にしていきたいと思います。

「Rails いいね 実装」などでググると以下の記事を始めとして色々な方が解説してくれている記事を見つけることができます。
- [Ruby on Rails いいね機能の実装と解説（Rails Tutorial 14章 演習の機能拡張）](https://qiita.com/jaramon/items/248bcb4b56e9fed8fc90)
- [[Rails]いいね機能の非同期での実装!!!](https://qiita.com/hayabusa3703/items/2b916e652a1dc85bb6e3)
- [Railsでいいね機能を実装しよう](https://qiita.com/nojinoji/items/2c66499848d882c31ffa)

Railsチュートリアルライクな実装方法はこれらを見れば理解できたので、これらの記事と少し方法を変えてみたいなというモチベーションから`.js.erb`を用いない方法でいいね機能を実装してみました。

# 前提
- スパイスカレー屋の検索・共有アプリを作成しており、UserがShopにいいねできるようにしたいという状況を想定しています。
- 既にUserリソースとShopリソースは作成されている状態から始めます。
- 認証系にはDeviseを用いており、`current_user`メソッドや`user_signed_in?`メソッド等が使用できる状態であるとします。
- ビューは`.erb`ではなく、`.slim`を使って書きます。
- 本記事ではいいねボタンはBootstrap4で簡単にスタイリングします。
- 最終的には以下のように動くことを狙います。

![ダウンロード (1).gif](https://qiita-image-store.s3.amazonaws.com/0/307533/0a9ea3fb-0dc2-7d98-b08c-0a84923802c9.gif)

# 開発環境
- Ruby 2.5.1
- Rails 5.2.2

# いいね実装の具体的な流れ

## 必要なGemのインストール

jQueryが使えるようにあらかじめ`'jquery-rails'`を入れておきます。

```ruby:Gemfile
gem 'jquery-rails'
```

そして`application.js`を以下のようにしておきます。
（順番に気を付けないとコンソールでエラーが消えてくれないので注意です。）

```app/assets/javascripts/application.js
・・・
//= require jquery3
//= require rails-ujs
//= require popper
//= require turbolinks
//= require bootstrap-sprockets
//= require_tree .
```

## Likeモデルの作成・編集

まずLikeモデルを作成していきます。

```:>_ターミナル
$ rails g model Like user:references shop:references
```

モデル生成時にデータ型を`references`としておくことで、以下のように何もしなくてもLikeモデルがUserとShopと関連付いていると認識してくれるようになるので便利です。

```ruby:app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :shop
end
```

いいね機能において、UserとShopの関係は1:1なので、マイグレーションファイルに`add_index :likes, [:user_id, :shop_id], unique: true`を追記しておきます。

その後、`$ rails db:migrate`を実行します。

```ruby:db/migrate/[timestamp]_create_likes.rb
class CreateLikes < ActiveRecord::Migration[5.2]
  def change
    create_table :likes do |t|
      t.references :shop, foreign_key: true
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :likes, [:user_id, :shop_id], unique: true
  end
end
```

次に各モデルに関連付けを行います。
Shopモデルにおける`like_users`は後で使うのでひとまずこのような形で記述しておいて下さい。

```ruby:app/models/user.rb
class User < ApplicationRecord
  ・・・
  has_many :likes, dependent: :destroy
  has_many :like_shops, through: :likes, source: :shop
  ・・・
end
```

```ruby:app/models/shop.rb
class Shop < ApplicationRecord
  ・・・
  has_many :likes, dependent: :destroy
  has_many :like_users, through: :likes, source: :user
  ・・・
end
```

## Likesコントローラの作成・ルーティング設定

コントローラを作成します。

```:>_ターミナル
$ rails g controller Likes
```

そしてルーティングを設定します。

今回はcreateアクションのみで実装しようと思うのでcreateだけあればOKです。

```ruby:
Rails.application.routes.draw do
  ・・・
  resources :likes, only: [:create]
  ・・・
end
```

## Likesコントローラ実装

さて、コントローラの中身を実装していきます。

createアクション＋toggleメソッドでいいねといいねの解除を表現しています。

いいねの保存や削除が上手く行かなかったら422のレスポンスを返して問題なく機能すれば200のレスポンスを返すというとてもシンプルな構造です。

```ruby:app/controllers/likes_controller.rb
class LikesController < ApplicationController
  def create
    @shop = Shop.find(params[:shop_id])
    @like = current_user.likes.find_by(shop: @shop)
    toggle
  end

  private

    def toggle
      if @like
        return head :unprocessable_entity unless @like.destroy
      else
        @like = current_user.likes.build(shop: @shop)
        return head :unprocessable_entity unless @like.save
      end
      head :ok
    end
end
```

## ビューの編集

いいねボタンといいね解除ボタンについて記述していきます。
今回は`likes/_like.html.slim`というパーシャルを作ってそこにボタンのビューを書きました。

ポイントを箇条書きします。

- ユーザーがログインしている時だけいいねボタンが表示されるように`user_signed_in?`メソッドを用いて表現しています。
- いいねボタンを押した時にJavaScript(jQuery)が動くように`id="link-mark"`を用意しています。
- Bootstrap4でボタンのスタイリングをしています。
- `data-shop_id="#{@shop.id}"`は後程出てくる`like.js`に対象店舗のid情報を送信するために必要です。
- `like?`メソッドでユーザーが対象の店舗に既にいいねしているかの判断をしています。

```app/views/likes/_like.html.slim
- if user_signed_in?
  - unless @shop.like?(current_user)
    a#link-mark.btn.btn-primary.text-white data-shop_id="#{@shop.id}" いいね！
  - else
    a#link-mark.btn.btn-secondary.text-white data-shop_id="#{@shop.id}" いいね！を取り消す
```

そして、`like?`メソッドは自作しなければならないので、Shopモデルで実装していきます。

各モデルを関連付けしたことによって`include?`メソッドが使用できるようになっているので、それを用いてユーザーが対象店舗に既にいいねをしているかしていないかを`true`または`false`で返しています。

```ruby:app/models/shop.rb
class Shop < ApplicationRecord
  ・・・
  has_many :likes, dependent: :destroy
  has_many :like_users, through: :likes, source: :user
  ・・・
  def like?(user)
    like_users.include?(user)
  end
end
```

## `like.js`の作成

さて、これでいいねボタンとLikesコントローラのcreateアクションの実装まで終えました。

あとは、いいねボタンを押したらLikesコントローラのcreateアクションで処理が行われるようにJavaScript(jQuery)の実装をしてビューとコントローラを繋いでいけば狙い通りの動きをしてくれるはずです。

そこで、先程のlikeパーシャルの最後に`<script>`　タグを生やすために`javascript_include_tag`ヘルパーを用います。

```app/views/likes/_like.html.slim
・・・
= javascript_include_tag('like')
```

`javascript_include_tag(like)`は`app/assets/javascripts`フォルダの中にある`like.js`ファイルを呼出しますよという意味なので、`$ touch app/assets/javascripts/like.js`のようにしてファイルを作成します。

そして以下のようにAjax処理を記述してあげれば完成です。
実装内容についてはコメントを付けているのでそちらを参照して下さい。

```app/assets/javascripts/like.js
$(function(){
  // id="link-mark"の箇所(いいねボタン)をクリックしたら
  $('#link-mark').on('click', function(){
    // 非同期でlikes#createに処理を送信＋その時に店舗情報(shop_id)を渡す
    $.ajax({
      url: '/likes',
      type: 'POST',
      data: {shop_id: $(this).data('shop_id')}
    })
    // 処理が上手く行ったらボタンを切り替えて
    .done((data) => {
      if ($(this).text() === 'いいね！') {
        $(this).text('いいね！を取り消す').removeClass('btn-primary').addClass('btn-secondary');
      } else if ($(this).text() === 'いいね！を取り消す') {
        $(this).text('いいね！').removeClass('btn-secondary').addClass('btn-primary');
      }
    })
    // 処理が上手く行かなかったら失敗の旨を伝えるアラートを表示
    .fail((data) => {
      alert('いいね！に失敗しました');
    })
  });
});
```

最終的にはきちんとボタンを押したら非同期でボタンが切り替わって、Likesテーブルにいいねが追加されたり削除されたりするようになります。（このgifではLikesテーブルにいいねが追加されたり削除されたりする様子は分かりませんが。。。笑）

![ダウンロード (1).gif](https://qiita-image-store.s3.amazonaws.com/0/307533/0a9ea3fb-0dc2-7d98-b08c-0a84923802c9.gif)

# まとめ
- `.js.erb`を用いない方法でいいね機能を実装してみました。
- 色々調べたり聞いたりしながら、RailsにおけるAjax処理の実装方法のパターンを増やせるようになったので大きな収穫でした。

# 参考URL
- [Ruby on Rails いいね機能の実装と解説（Rails Tutorial 14章 演習の機能拡張）](https://qiita.com/jaramon/items/248bcb4b56e9fed8fc90)
- [[Rails]いいね機能の非同期での実装!!!](https://qiita.com/hayabusa3703/items/2b916e652a1dc85bb6e3)
- [Railsでいいね機能を実装しよう](https://qiita.com/nojinoji/items/2c66499848d882c31ffa)