【Rails】いいね機能をjQueryで実装

## はじめに
一覧ページでのいいね機能を実装しようとしましたが、一番上の星をいいねしたら全てのチームにいいねがついてしまうという問題が発生しました。この問題が起きたことで大変学習になったので、思考の整理とアウトプットとして記事にまとめさせていただきました。また、いいね機能は多くのアプリで実装することが多いと思います。同じような問題が起きている方や別の実装方法など、ご意見いただけたら幸いです！

## 概要
まず大前提にテーブルは、`users`, `teams`, `likes`の3つで`likes`は
`users`と`teams`の中間テーブルになっています。
AjaxでHTTPメソッドの`GET`, `POST`, `DELETE`ができるように実装していきます。

## 起きている問題・解決したいこと（仮説）

起きている問題と解決したいことを簡単に書いていきたいと思います。

#### 起きている問題
- 全ての記事にいいねがついてしまう。
- 全てのチームのidを取得していたつもりが一番上のチームのidしか取得できていない。（デベロッパーツールで確認）

#### 解決したいことと仮説
- 配列にして取得できていると思ったができていないので配列から一つずつチームを取得できるようにする。
- いいね対象を判断できるようにクラスをつけないといけない。
- クリックした対象に動的idが付与されていれば実装できるのではないか。

## Ajaxで実装すること

- いいねの表示
- いいねされたら黄色い星を表示
- いいねされているものをクリックしたら白い星を表示

## LikesController

`show`, `create`, `destroy`アクションを使用しています。
Railsでの定義と違うところはリクエストに対して返すのがjson形式のデータということです。

```ruby:app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_like

  def show
    like_status = current_user.has_liked?(@team)
    render json: { hasLiked: like_status }
  end
  

  def create
    @team.likes.create!(user_id: current_user.id)
    
    render json: { status: 'ok' }
  end

  def destroy
    like = @team.likes.find_by!(user_id: current_user.id)
    like.destroy!

    render json: { status: 'ok' }
  end

  private

  def set_like
    @team = Team.find(params[:team_id])
  end

end
```
## user.rb

`has_liked?`は`user.rb`で定義しています。showアクション内の処理のように`current_userがteamをlikeしてる？`と直感的にわかりやすいと思い、定義しております。

```ruby:app/models/user.rb
def has_liked?(team)
  likes.exists?(team_id: team.id)
end
```

## index.html.haml

HTTPリクエストを送るにはJavaScriptでidを取得できるようにしなくてはいけないので、テンプレートにカスタムデータを記述しています。`data-<情報>`とすることで任意の属性を付与することができます。今回はチームに対していいねをしたいので`team`の`id`が必要です。<br>※`routes`を確認するとどこにリクエストを送れば良いかよくわかります。

たとえばですが、以下`index.html.haml`のように記述してデベロッパーツールで確認すると

- `data-team-id="1"`
- `id="active-star1"`

このようにdata属性とチームのidが入ったidを取得することができます。
チームのidが入ったidは一覧ページでのいいね機能なので、クリックしたときに特定のチームをいいねするという状況になります。それを判定するために記述しております。実際にjQueryのコードを見ていただいたほうが早いと思いますが、簡単に説明させていただきますと、`active-star`という`id`だけだと、`active-star`というidは複数あるので、全てのチームにいいねをつける処理が実行されてしまいます。そのため、`動的なid(今回はチームのid)`を付与しております。数字の`1`の部分がチームのidです。

### ここで重要なのは、
- カスタムデータ
- hiddenでいいねの表示を隠していること

hiddenを使っている理由はいいねの状態を確認し表示するということをAjaxで表示するためです。

```haml:index.html.haml
.hidden.active-star{id: "active-star#{teams.id}", data: {team_id: tams.id}}
          = image_tag 'star-yellow.png'
.hidden.in-active-star{id: "in-active-star#{teams.id}", data: {team_id: teams.id}}
          = image_tag 'star-white.png'
```

```scss:css
.hidden {
  display: none;
}
```

## jQuery

本題のいいね機能の実装部分です。
記述が冗長になってしまったので、コメントを入れております。重要なのは`csrfToken`をリクエスト時に持たせることです。これをしないと`422 (Unprocessable Entity)というエラー`が起きます。なぜかというとGETと違ってPOSTなどの処理はデータベースの変更をするリクエストなので、簡単に操作されては困るため制約がついています。そのため、`rails-ujs`を使って`axios`でリクエスト時に`csrfToken`というのを持たせるようにしております。

```javascript:jQuery
import $ from 'jquery'
import axios from 'axios'

import { csrfToken } from 'rails-ujs'
// リクエスト時にCSRFトークンを持たせる
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()

document.addEventListener('DOMContentLoaded', () => {

  // ロード時にいいねされていない星を配列で取得
  $('.in-active-star').each(function (index, element) {
    // テンプレートで記述したカスタムデータを取得
    const likeData = $(element).data()
    // カスタムデータからチームIDを取得
    const teamId = likeData.teamId
    // カスタムデータを入れてGETリクエストを送る
    axios.get(`/teams/${teamId}/like`)
      // リクエストを送ったらレスポンスが返ってくる
      .then((response) => {
        // responseでrenderされたlikeの状態を取得(true or false)
        const inActiveStatus = response.data.hasLiked
        // falseであればいいねされていない => 白い星を表示するために、'hidden'を取り外す
        if ( inActiveStatus === false ) {
          $(element).removeClass('hidden')
        } 
      })
  })

  // ロード時にいいねされている星を配列で取得
  $('.active-star').each(function (index, element) {
    // テンプレートで記述したカスタムデータを取得
    const likeData = $(element).data()
    // カスタムデータからチームIDを取得
    const teamId = likeData.teamId
    // カスタムデータを入れてGETリクエストを送る
    axios.get(`/teams/${teamId}/like`)
      // リクエストを送ったらレスポンスが返ってくる
      .then((response) => {
        // responseでrenderされたlikeの状態を取得(true or false)
        const activeStatus = response.data.hasLiked
        // trueであればいいねされている => 黄色い星を表示するために、'hidden'を取り外す
        if ( activeStatus === true) {
          $(element).removeClass('hidden')
        } 
      })
  })

  // #create いいねをつけたいときの処理
  $('.in-active-star').on('click', (e) => {
    e.preventDefault();
    const dataset = $(e.currentTarget).data()
    // クリックした要素のidを取得
    const teamId = dataset.teamId
    // teamIdを使いPOSTリクエストを送る
    axios.post(`/teams/${teamId}/like`)
    .then((response) => {
      // リクエスト成功なら処理を行う
      if (response.data.status === 'ok') {
        $(`#in-active-star${teamId}`).addClass('hidden');
        $(`#active-star${teamId}`).removeClass('hidden');
      }
    })
    // エラー時の処理
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })

  })

  // #destroy いいねを外したいときの処理
  $('.active-star').on('click', (e) => {
    e.preventDefault();
    const dataset = $(e.currentTarget).data()
    // クリックした要素のidを取得
    const teamId = dataset.teamId
    // teamIdを使いdeleteメソッドを使う
    axios.delete(`/teams/${teamId}/like`)
    .then((response) => {
      // リクエスト成功なら処理を行う
      if (response.data.status === 'ok') {
        $(`#active-star${teamId}`).addClass('hidden');
        $(`#in-active-star${teamId}`).removeClass('hidden');
      }
    })
    // エラー時の処理
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })
  })

})
```

## まとめ
- Ajax処理はデベロッパーツールを使い`debugger`や`console.log()`を使い値が取れているか確認をしっかりすると開発が捗る。
- 詳細ページのように一つしかいいねがないときは意識していなかったが、一覧ページなど複数ある中の一つというように特定したいときは個別のidを付与することで実装できる。
- POSTやDELETE時にはデータベースの操作をすることからCSRFトークンというものが必要になる。

## 最後に
今回はjQueryを使って実装しました。Vue.jsの学習も始めたので、生JSやjQueryをもっと理解したら開発に使っていきたいなと思っています。このように実装するのもひとつの方法だと思いますが、他にもたくさんの実装方法があると思います。他のライブラリやフレームワークではどのように実装するのか、また一度書いたコードもリファクタリングを積極的にやっていけたらと思います！


## 参考文献
- [ES6のアロー関数でjQueryのイベント内のthisがundefinedになるのに対処する](https://webmanab-html.com/tip/es6-jquery-this-undefined/)
- [[jQuery]でcssプロパティを動的に追加、変更、削除する](https://qiita.com/kazTera/items/ab5dd9fb5b2579b25c4d)
- [jQuery if文のサンプル](https://itsakura.com/jquery-if)
- [hamlで動的にclass名/idを設定する](https://qiita.com/nowin/items/1096006e35a6fe9cf785)
- [jQueryのattrメソッド](https://qiita.com/gestalt/items/b5317984015a2bb6b745)
- [jQueryのattrメソッドの使い方を現役エンジニアが解説【初心者向け】](https://techacademy.jp/magazine/26775)
- [Rails で JavaScript を使用する](https://railsguides.jp/working_with_javascript_in_rails.html)
- [【jQuery入門】2種類のeach()による繰り返し処理まとめ！](https://www.sejuku.net/blog/33609)
- [【for/forEach/jQuery.each】JavaScriptでforeachするためのコードと速度の比較！](https://www.creativevillage.ne.jp/72411)