View側の繰り返し処理に対応してJavaScriptで動的にidを取得する方法

## はじめに
インスタ風のアプリを作成している中で、Ralis側で繰り返し処理をしている箇所にJavaScriptでcssクラスをつけたり外したりする処理　などの実装に苦戦したので、実装方法を整理してアウトプットします。

## やりたいこと

記事の一覧画面でAjaxを使って、いいねができるようにしたい

### 問題
show.htmlのようなpost_idが一意に決まっているようなページをJavaScriptで操作するのではなく、
index.htmlのようなpostがたくさん表示されているページから各ポストのidを個別に指定して操作していく実装がわからなかったです。


#### viewはこんな感じ
```ruby
- @posts.each do |post|
  .card
    = image_tag post.user.profile.avatar
    = show_posttime(post)
    = post.user.username

    - post.images.each do |image|
      = image_tag image

    = post.caption
    = image_tag 'likeBtn'
    = image_tag 'unlikeBtn'

    = image_tag 'commentBtn'

    = link_to "https://twitter.com/intent/tweet?url=#{URI.encode_www_form_component(request.original_url)}&text=#{URI.encode_www_form_component(post.caption)}", rel: 'nofollow', target: '_blank' do
      = image_tag 'shareBtn'
```
postがたくさん吐き出されるので、
どのポストなのかJavaScript側にどう伝えたらいいかに悩みました。


## やり方
### 方針
JavaScript側でも繰り返し処理を行う

### hamlにid,classを追加する
```ruby
- @posts.each do |post|
  .card.post-index{data: {post_id: post.id}}
    .
    .
    .
    = image_tag 'likeBtn', class: 'hidden', id: "likeBtn_#{post.id}"
    = image_tag 'unlikeBtn', class: 'hidden', id: "unlikeBtn_#{post.id}"
```
- `post-index`をidでなくclassにしているのは、javascript側で`.post-index`がついている要素をeachで繰り返し処理をするため
- Btnの出し分けは、javascriptでaddClass,RemoveClassを使って実行したいため、動的にidを付与

#### dataの使いどき
- JavaScriptでHTMLを操作したいときにdata属性が便利
- 今回のケースのように個別の投稿からidを取得することもできる
- 今回は違う実装をしてしまったけど、Btnにdata属性を指定したら、クリックしたときにdata属性に付与したidを取得してリクエストに使うこともできる
#### 具体的なdataの使い方
- hamlで対象の要素にdata属性を付与する
- haml: `data: {post_id: post.id}`  => html: `data-post-id="1"` 
=>javascript: `dataset{postID: "1"}`　という感じに変換される
- jQueryでは`$('.post').data()`でdataの中身を取得できる

### JavaScriptファイルを記述する
```js
$(document).on('turbolinks:load', () => {
  $('.post-index').each(function() {    //hamlの繰り返しに対応して、eachの繰り返し処理を行う
    const dataset = $(this).data()    //thisはここでは繰り返しされているなかで出てきてる一つの要素
    const postId = dataset.postId

    axios.get(`/posts/${postId}/like`)   //リクエスト先のPathを動的に設定
    .then(response => {
      if (response.data.has_like === true) {  //バックエンド側のレスポンスとして、likeの有無を確認
        $(`#unlikeBtn_${postId}`).removeClass('hidden')
        $(`#likeBtn_${postId}`).addClass('hidden')
      } else {
        $(`#unlikeBtn_${postId}`).addClass('hidden')
        $(`#likeBtn_${postId}`).removeClass('hidden')
      }
    })

    $(`#likeBtn_${postId}`).on('click', () => {
      axios.post(`/posts/${postId}/like`)
      .then(response => {
        if (response.data.has_like === true) {
          $(`#unlikeBtn_${postId}`).removeClass('hidden')
          $(`#likeBtn_${postId}`).addClass('hidden')
        } else {
          $(`#unlikeBtn_${postId}`).addClass('hidden')
          $(`#likeBtn_${postId}`).removeClass('hidden')
        }
      })
    })

    $(`#unlikeBtn_${postId}`).on('click', () => {
      axios.delete(`/posts/${postId}/like`)
      .then(response => {
        if (response.data.has_like === true) {
          $(`#unlikeBtn_${postId}`).removeClass('hidden')
          $(`#likeBtn_${postId}`).addClass('hidden')
        } else {
          $(`#unlikeBtn_${postId}`).addClass('hidden')
          $(`#likeBtn_${postId}`).removeClass('hidden')
        }
      })
    })
  })
})

```
- ポイントは繰り返し処理をすること
- 繰り返しをしないと1番上に表示するポストにだけJavaScriptが適用されてしまう
- それ以外の記述は基礎的な感じ

### 一応Controller（バックエンド）の記載
```ruby
class LikesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_like, only: [:show, :destroy]

  def show
    # もしそのポストのlikeが存在したらtrue,しなければfalseを返す
    render json: {
      has_like: @like.present?
    }
  end

  def create
    like = current_user.likes.build(post_id: params[:post_id])
    like.save!
    render json: {
      has_like: like.present?
    }
  end

  def destroy
    @like.destroy!
    render json: {
      has_like: !@like.destroyed? #destroyをしてもpresent?で確認するとメモリ上のオブジェクトは残っているのでtureで返されてしまう
    }
  end

  private
  def set_like
    @like = current_user.likes.find_by(post_id: params[:post_id])
  end

end

```
- そんなに難しいことはしてない
- 唯一特殊なのが、`has_like: !@like.destroyed?`の記述
- `has_like: like.present?`で通したかったけど、destroy実行後もこの書き方だとtrueが返ってきてしまった
- メモリ上オブジェクトが残ってしまうため、とのこと


## おわりに
わりといろんなWebアプリでインデックスページでいいねができたり、コメントできたりするのが普通だと思うのでその実装方法を学べたのが良かったです。

view側での繰り返し処理をする場合のjavascriptの適用方法という感じで抽象化しても、色んな場面で使えそう。

本編には関係ないですが、data属性をうまく使うともっとJavaScriptの記述をシンプルにできた気がするので、今度はそのへんも深めていきたいと思います。