[Rails] Ajaxで作るクリップ機能(いいね機能)

ポートフォリにAjaxでいいね機能を実装した時のメモです。
### Ajaxとは？
JavaScriptを使ってサーバーにリクエストを投げて、レスポンスに基づきHTMLに変更を加える技術のことらしい。
分かりやすくいうとページのリロードなしで表示が切り替わるってこと。
身近なのはやっぱりTwitterのいいねですかね。

多対多のクリップ機能は既に実装済みでとします。
ちなみに今回の場合は、以下のようにUserモデルとEventモデルの中間テーブルとしてClipモデルがあることとしてます。

![スクリーンショット 2021-10-08 16.27.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974719/528ca12e-9481-f2dd-d6d2-257c356ee438.png)


# 完成イメージ
クリップしたときにリロードなしでクリップできていてクリップしたイベントとしてクリップイベント一覧に反映されている。
![ezgif.com-gif-maker (5).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974719/7e78bc64-b21f-0c4e-d0b2-6be22cc0b4bd.gif)


# 実装

### ① jQuery導入
jQueryを導入していきます。 

```terminal:ターミナル 
yarn add jQuery
``` 
`yarn`とはJavaScriptのライブラリを管理するもの。Rubyでいうgemのようなもの。
`npm`というのもあるがRailsでは基本的に`yarn`を使うらしい。

次にwebpackerのサーバーを立ち上げます。

```terminal:ターミナル
bin/webpack-dev-server
```
`webpacker`とは`webpack`のRails版みたいなもので複数のJavaScriptファイルを読み込んで1つのファイルに出力してくれます。
これをすることでJavaScriptの読み込みを早くすることができ開発スピードを上げることができます。

それでは動作確認をしていきます。

```javascript:app/javascript/packs/application.js
import $ from 'jquery'

document.addEventListener('DOMContentLoaded', () => {
  $('.title').on('click', () => {
    window.alert('CLICKED')
  })
}) 
```

確認方法は何でもいいんですが、今回は`title`というクラスのついた部分をクリックしたらアラートが出るようにして動作を確認していきます。
以下のようにタイトルをクリックしてアラートが出てればOKです。

### ② クリップの状態を非同期で取得する
まずはアクセスした時にイベントがクリップしてるのかしてないかの状態を判断するAPIを作っていきます。

クリップしているかどうかのメソッドを作って、

```ruby:app/models/user.rb
def has_clipped?(event)
  clips.exists?(event_id: event.id)
end
```

現状の`clip_controller.rb`にshow actionを追加します。

```ruby:config/routes.rb 
resource :clip, only: [:show, :create, :destroy]
```

```ruby:app/controllers/clips_controller.rb
class ClipsController < ApplicationController
  before_action :authenticate_user!

#-------------------------追加-----------------------
  def show
    event = Event.find(params[:event_id])
    clip_status = current_user.has_clipped?(event)
    render json: { hasClipped: clip_status }
  end
#---------------------------------------------------

  def create
    event = Event.find(params[:event_id])
    event.clips.create!(user_id: current_user.id)
    redirect_to event_path(event)
  end
  def destroy
    event = Event.find(params[:event_id])
    clip = event.clips.find_by!(user_id: current_user.id)
    clip.destroy!
    redirect_to event_path(event)
  end
end
```
やっていることはアクセスしたイベントがクリップされているかいないかを判断しています。
クリップしているイベントには`{"hasCliped":true}`というデータがjsonで返されます。
このステータス状態によってHTMLを変更していきます。

axiosをインストールします。

```terminal:ターミナル 
yarn add axios
```
axiosでgetリクエストをしてステータス状態を取得します。

その前に、イベントのステータス状態を取得するのには`event_id`が必要なのでクリップボタンを設置するHTML部分に以下を記述することで`event_id`の取得ができます。

```haml:app/views/events/show.html.haml
.container#event-show{data: {event_id: @event.id}}
```

あとは取得した`event_id`を使ってgetリクエストしていきます。

```javascript:app/javascript/packs/application.js 
import axios from 'axios'

document.addEventListener('DOMContentLoaded', () => {
  const dataset = $('#event-show').data()
  const eventId = dataset.eventId
  axios.get(`/events/${eventId}/clip`)
    .then((response) => {
      console.log(response)
      const hasClipped = response.data.hasClipped
    })
}) 
```
axiosのレスポンスの結果次第でクリップボタンの表示をJavaScriptで切り替えできるようにしていきます。

```ruby:app/views/events/show.html.haml
.clip
  - if user_signed_in?
    .clip__active.hidden.active-clip クリップ中
    .clip__icon.hidden.inactive-clip クリップする
```
```ruby:app/assets/stylesheets/application.scss
.hidden {
  display: none;
}
```

クリップボタンの部分のHTMLに`hidden`クラスを両方につけ`display: none;`のcssを当てます。

```javascript:app/javascript/packs/application.js
const handleClipDisplay = (hasClipped) => {
  if (hasClipped) {
    $('.active-clip').removeClass('hidden')
  } else {
    $('.inactive-clip').removeClass('hidden')
  }
}
```
axiosのレスポンスのステータス状態に合わせて`hidden`クラスをつけるか外すかを決めることでクリップボタンのHTMLが変化します。
そしてこのfunctionを先程のgetリクエスト部分に記述します。

```javascript:app/javascript/packs/application.js 
import axios from 'axios'

document.addEventListener('DOMContentLoaded', () => {
  const dataset = $('#event-show').data()
  const eventId = dataset.eventId
  axios.get(`/events/${eventId}/clip`)
    .then((response) => {
      console.log(response)
      const hasClipped = response.data.hasClipped
     //-------追加-------------------
      handleClipDisplay(hasClipped)
    //------------------------------
    })
}) 
```
これで非同期でクリップしているかしていないかを取得することができます。

### ③非同期でクリップする
次は実際にクリップするpostリクエスト部分とクリップを外すdeleteリクエスト部分を作っていきます。

APIを作っていきます。
リダイレクトしていた部分をrenderできるように変更していきます。

```ruby:app/controllers/clips_controller.rb
class ClipsController < ApplicationController
  before_action :authenticate_user!

  def show
    event = Event.find(params[:event_id])
    clip_status = current_user.has_clipped?(event)
    render json: { hasClipped: clip_status }
  end

  def create
    event = Event.find(params[:event_id])
    event.clips.create!(user_id: current_user.id)
     render json: { status: 'ok' } #変更部分
  end
  def destroy
    event = Event.find(params[:event_id])
    clip = event.clips.find_by!(user_id: current_user.id)
    clip.destroy!
     render json: { status: 'ok' } #変更部分
  end
end
```

axiosでpostリクエストを作る前にターミナルで以下をインストールして`application.js`に記述します。

```terminal:ターミナル 
yarn add rails-ujs
```

```javascript:app/javascript/packs/application.js
import { csrfToken } from 'rails-ujs'

axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()
```

ここでやっているのはセキュリティ対策です。
postリクエストの場合はgetリクエストと違いセキュリィティ的に問題があるので上の記述がないとpostリクエストが通りません。
`csrfToken`というのは鍵みたいなもので、その鍵をaxiosのときはデフォルトで持たせるように記述しています。

postリクエスト部分

```javascript:app/javascript/packs/application.js 
$('.inactive-clip').on('click', () => {
  axios.post(`/events/${eventId}/clip`)
    .then((response) => {
      if (response.data.status === 'ok') {
        $('.active-clip').removeClass('hidden')
        $('.inactive-clip').addClass('hidden')
     }
     console.log(response)
    })
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })
})

```

`.then((response) => {}`はリクエストがうまくいった時の処理。今回はリクエストがうまくいったらクリップした状態にボタンを変更させています。
`.catch((e) => {}`の部分はリクエストが失敗した時の処理。今回は`window.alert`でErrorという文字列を表示させます。

最後にクリップを外すdeleteリクエスト

```javascript:
$('.active-clip').on('click', () => {
  axios.delete(`/events/${eventId}/clip`)
    .then((response) => {
      if (response.data.status === 'ok') {
        $('.active-clip').addClass('hidden')
        $('.inactive-clip').removeClass('hidden')
      }
       console.log(response)
     })
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })
})
```
やっているのはpostの逆の処理です。

動作確認をして画面がリロードせずクリップできていたら完成です！