【Rails】Ajax(非同期通信)でコメント編集

前回のコメント投稿と削除はremote trueをリンクボタンに設定して実装しました。
[【Rails】Ajax(非同期通信)でコメント投稿、削除](https://qiita.com/hiro266/items/56ec2c350fd9d8ca22d8)
今回はコメントの編集をajax関数を使用して実装していきます。

注))前回からの続きです

# 環境
`Rails 5.2.3`
`mysql 5.7.28`
`gem jquery-rails`
→application.jsでrequireしておく。

# 完成デモ

[![Image from Gyazo](https://i.gyazo.com/93fc87bf0f8df887d45539b989c6306f.gif)](https://gyazo.com/93fc87bf0f8df887d45539b989c6306f)

# 実装
機能を四つに分けて実装していきます。
1. 編集ボタンクリックでラベル非表示、コメントエリアとボタン表示
2. キャンセルボタンクリックでラベル表示、コメントエリアとボタン非表示
3. 更新ボタンクリック<font color="Green">**成功**</font>でDB更新、ラベル表示、コメントエリアとボタン非表示
4. 更新ボタンクリック<font color="Red">**失敗**</font>でエラーメッセージ表示

## 1. 編集ボタンクリックでラベル非表示、コメントエリアとボタン表示

[![Image from Gyazo](https://i.gyazo.com/602b9cb06d071c90905c1c34d2d66126.gif)](https://gyazo.com/602b9cb06d071c90905c1c34d2d66126)

```ruby:編集ボタン
<span data-comment-id=<%= comment.id %> class="js-edit-comment-button">
  <i class="fas fa-edit text-primary"></i>
</span>
```

```ruby:一意のコメント
<div id="js-comment-<%= comment.id %>">
  <p id="js-comment-label-<%= comment.id %>"><%= comment.body %></p>
  <p id="js-comment-post-error-<%= comment.id %>" class="text-danger"></p>
  <textarea style="display: none;" id="js-textarea-comment-<%= comment.id %>" class="form-control comment-post-error"><%= comment.body %></textarea>
  <div id="js-comment-button-<%= comment.id %>" style="display: none;">
    <button data-cancel-id=<%= comment.id %> type="button" class="btn btn-light comment-cancel-button">キャンセル</button>
    <button data-update-id=<%= comment.id%> type="submit" class="btn btn-success comment-update-button">更新</button>
  </div>
</div>
```

```ruby:application.js
# コメント編集エリア表示
$(function () {
  $(.js-edit-comment-button).on("click", function () {              # ①クリックイベント
    const commentId = $(this).data('comment-id');                   # ②一意のcomment.idを代入
    const commentLabelArea = $('#js-comment-label-' + commentId);   # ③一意のラベルを代入
    const commentTextArea = $('#js-textarea-comment-' + commentId); # ④一意のコメントエリアを代入
    const commentButton = $('#js-comment-button-' + commentId);     # ⑤一意のボタンエリアを代入

    commentLabelArea.hide(); # ③を非表示
    commentTextArea.show();  # ④を表示
    commentButton.show();    # ⑤を表示
  });
});
```

① `js-edit-comment-button`をクリックしたら中身を処理する条件定義。

② data属性を使用して`data-comment-id`に設定した`comment.id`を取得。ラベル、コメントエリア、ボタンエリアの各要素をそれぞれ一意にしないと`js-edit-comment-button`をクリックした際に各コメントの全ての各要素が反応してしまいます。そのため各要素は一意にする必要があり、③~⑤で実装していくための前段階として`comment.id`を取得しておきます。

③ id属性`js-comment-label-`に、②で定義した`comment.id`を渡します。

④ id属性`js-textarea-comment-`に、②で定義した`comment.id`を渡します。

⑤ id属性`js-comment-button-`に、②で定義した`comment.id`を渡します。

それ以下の処理がわからない方は[Progate jQuery編](https://prog-8.com/languages/jquery)でハンズオンできるのでやってみてください。

ただ、この①の記述だと問題点があります。後にコメントを追加した時にリロードしないと編集ボタンを押しても反応しません。これは追加されたコメントにはイベントが設定されないことが原因です。

```ruby:application.js
# $(.js-edit-comment-button).on("click", function () {
$(document).on("click", ".js-edit-comment-button", function () {
```

こう記述することでHTMLをスクリプト(js)で追加した後に追加要素にもイベントを設定することができます。
つまり、追加した要素の編集ボタンも動作します！

### 変数の中身確認
`application.js`の中で理解が難しいのが②の部分だと思います。
本当に`comment-id`が取得できるのか試してみます。

```ruby:application.js
# コメント編集エリア表示
$(function () {
  $(.js-edit-comment-button).on("click", function () {
    const commentId = $(this).data('comment-id');
    console.log(commentId);
    ・
    ・
    ・
  });
});
```

この状態でいくつかコメントを作って`js-edit-comment-button`をクリックしてビューの検証ツールを開いてみましょう。
command + option + iで検証ツールを開いてConsoleタブに移動すると

[![Image from Gyazo](https://i.gyazo.com/3f59b444c58a5150d063403804df6fcf.png)](https://gyazo.com/3f59b444c58a5150d063403804df6fcf)

こんな感じでクリックのたびに一意な数字が表示されていればidが取得できてるなーと確認が取れます。


## 2. キャンセルボタンクリックでラベル表示、コメントエリアとボタン非表示

[![Image from Gyazo](https://i.gyazo.com/1b3e0cea349340dd01f916621a760539.gif)](https://gyazo.com/1b3e0cea349340dd01f916621a760539)

```ruby:一意のコメント
<div id="js-comment-<%= comment.id %>">
  <p id="js-comment-label-<%= comment.id %>"><%= comment.body %></p>
  <p id="js-comment-post-error-<%= comment.id %>" class="text-danger"></p>
  <textarea style="display: none;" id="js-textarea-comment-<%= comment.id %>" class="form-control comment-post-error"><%= comment.body %></textarea>
  <div id="js-comment-button-<%= comment.id %>" style="display: none;">
    # キャンセルボタン↓
    <button data-cancel-id=<%= comment.id %> type="button" class="btn btn-light comment-cancel-button">キャンセル</button>
    <button data-update-id=<%= comment.id%> type="submit" class="btn btn-success comment-update-button">更新</button>
  </div>
</div>
```

```ruby:application.js
# コメント編集エリア非表示
$(function () {
  $(document).on("click", ".comment-cancel-button", function () {
    const commentId = $(this).data('cancel-id');
    const commentLabelArea = $('#js-comment-label-' + commentId);
    const commentTextArea = $('#js-textarea-comment-' + commentId);
    const commentButton = $('#js-comment-button-' + commentId);
    const commentError = $('#js-comment-post-error-' + commentId);

    commentLabelArea.show();
    commentTextArea.hide();
    commentButton.hide();
    commentError.hide();
  });
});
```

キャンセルボタン(comment-cancel-button)をクリックしたら中身を処理する条件定義をして、中身は編集ボタンと同じような内容なので割愛します。

## 3. 更新ボタンクリック成功でDB更新、ラベル表示、コメントエリアとボタン非表示

ここからが今回の実装の肝であるajax関数を使った処理になります。
先に言っておくとコメント投稿と削除と同じように`remote true`を使用しても実装できますが、もっと複雑な処理の場合はajax関数を使用しての実装になるので慣れるという意味でajax関数を使用します。

[![Image from Gyazo](https://i.gyazo.com/e2650393a048aba292f034db66df08c7.gif)](https://gyazo.com/e2650393a048aba292f034db66df08c7)

```ruby:一意のコメント
<div id="js-comment-<%= comment.id %>">
  <p id="js-comment-label-<%= comment.id %>"><%= comment.body %></p>
  <p id="js-comment-post-error-<%= comment.id %>" class="text-danger"></p>
  <textarea style="display: none;" id="js-textarea-comment-<%= comment.id %>" class="form-control comment-post-error"><%= comment.body %></textarea>
  <div id="js-comment-button-<%= comment.id %>" style="display: none;">
    <button data-cancel-id=<%= comment.id %> type="button" class="btn btn-light comment-cancel-button">キャンセル</button>
    <button data-update-id=<%= comment.id%> type="submit" class="btn btn-success comment-update-button">更新</button>
    # 更新ボタン↑
  </div>
</div>
```

```ruby:application.js
# コメント更新ボタン
$(function () {
  $(document).on("click", ".comment-update-button", function () {
    const commentId = $(this).data('update-id');
    const textField = $('#js-textarea-comment-' + commentId); # ①
    const body = textField.val();                             # ②textFieldの内容を取得
  # console.log(body);

    $.ajax({                         # ③ajax関数
      url: '/comments/' + commentId, # ④comments/updateアクションにリクエスト
      type: 'PATCH',                 # ⑤HTTP通信の種類： updateアクションなので'PATCH'と記述
      data: {                        # ⑥commentモデルのbodyカラムに変数bodyを格納
        comment: {
          body: body
        }
      }
    })
  });
});
```

①までは割愛します。わからない方はもう一度記事を読み返してみてください。

②はコメントエリアの内容を変数`body`の中へ代入しています。
この下に`console.log(body);`と記述してコメントエリアに何かしら記述して更新ボタンを押してみてください。打ち込んだ内容が検証ツールのConsoleに表示されているはずです。

③からがajax関数を使った通信内容です。

④任意のアクションへのURIを記述。(今回はcomment/updateアクション)

⑥パラメーターとして送りたいデータを記述。

これで更新ボタンがクリックされたらcomments/updateアクションへパラメーターがリクエストされる状態まで記述ができました。
次はcomments_controllerでの処理に移ります。

```ruby:comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_comment, only: %i[update destroy]

def update
  # binding.pry
    @comment.update!(comment_params)
end


private

  def set_comment
    @comment = current_user.comments.find(params[:id])
  end

  def comment_params
    params.require(:comment)
          .permit(:body)
          .merge(board_id: params[:board_id])
  end

end
```

とりあえず`binding.pry`してここまでパラメーターきてるか確認しておきました。
さあ、やってみよう！
[![Image from Gyazo](https://i.gyazo.com/d791aa47312eb68c85228414b2716e04.gif)](https://gyazo.com/d791aa47312eb68c85228414b2716e04)

当然ダメですね。
まだまだやることがたくさんあります。
一つずつ潰していきます。

まずレスポンスで何が返ってきてるか調べてみます。検証ツール → Network → Responseを開くと`バリデーションに失敗しました: Boardを入力してください`との表示。

これは

```ruby:comments_controller.rb
def comment_params
  params.require(:comment)
        .permit(:body)
        .merge(board_id: params[:board_id])
end
```

`comment_params`でboard_idがパラメーターで渡ってくる想定でいるのにないんだけどどういうこと？`Boardを入力してください`と怒ってるわけです。
でもコメント投稿の時点で投稿とコメントは紐づいてるから今回はboard_idいらないんだよね。

というわけで、

```ruby:comments_controller.rb
def update
    @comment.update!(comment_update_params)
end

# 追加
def comment_update_params
  params.require(:comment)
        .permit(:body)
end
```
新たにメソッド作っちゃいましょう。
これで再チャレンジ！

[![Image from Gyazo](https://i.gyazo.com/b4a9a91347ac0be9b8c50b07a6182bed.gif)](https://gyazo.com/b4a9a91347ac0be9b8c50b07a6182bed)

ん〜〜〜
クリックしても反映されません。
リロードすると反映されるのでupdateメソッドは成功しているみたいですね。

これは現段階での更新ボタンがクリックされてからのJsファイルの処理は、変数を定義して、comments/updateアクションにパラメーターをリクエストしただけなので、ラベルを表示させたり、コメントエリアを非表示にさせたりという処理を書いてないからです。
次はJsファイルにこれらの記述をしていきます。

```ruby:application.js
# コメント更新ボタン
$(function () {
  $(document).on("click", ".comment-update-button", function () {
    const commentId = $(this).data('update-id');
    const textField = $('#js-textarea-comment-' + commentId); 
    const body = textField.val();
  # console.log(body);

    $.ajax({
      url: '/comments/' + commentId,
      type: 'PATCH',
      data: {
        comment: {
          body: body
        }
      }
    })
# 追加
      .done(function () { # ①ajax通信が成功した時の処理
        const commentLabelArea = $('#js-comment-label-' + commentId);
        const commentTextArea = $('#js-textarea-comment-' + commentId);
        const commentButton = $('#js-comment-button-' + commentId);
        const commentError = $('#js-comment-post-error-' + commentId); # ②

        commentLabelArea.show();
        commentLabelArea.text(???); # ③データベースにupdateされたbodyのvalueが欲しい
        commentTextArea.hide();
        commentButton.hide();
        commentError.hide();
      })
# 追加
  });
});
```
①`.done`の中では、コントローラーでの処理が成功した時、この中の処理を実行する。

②までは今までと同じような処理なので割愛。

③ラベルにはコメントエリアに記述した内容(body)をtextとして差し替えれば良さそう。

では③をどうやって取得すれば良いか。
これは`.done`に返ってきた時、引数にcommentインスタンスを渡してもらってbody要素を取り出せば良さそうです。
なので、

```ruby:application.js
# 変更点のみ記述
.done(function (data) { 

  commentLabelArea.text(data.body);
  
})
```

こう書いたら良さそうです。
再チャレンジ！

[![Image from Gyazo](https://i.gyazo.com/c50c551b28350d7f0e9f464cab9a3fec.gif)](https://gyazo.com/c50c551b28350d7f0e9f464cab9a3fec)

ターミナルを見に行くとステータスコード204が返ってきていた。

> 204エラー引用
>> 204 No Content
訳すると内容なし。HTTPステータスコードとして204を返すのなら、その名の通りレスポンスボディーは何も返さないのが正解

とのこと。
レスポンスボディーに何も入っていない？
あっ...controllerから渡してあげないとダメか。
検証ツールのResponseを見に行くと見事に空っぽでした。
一応デバックして先ほどの引数の中身を見てみます。

```ruby:application.js
.done(function (data) { 
# 追加
 debugger;

・
・
・

})
```

検証ツールを開いてから更新ボタンを押すと、
[![Image from Gyazo](https://i.gyazo.com/9d8dde83ea0cd221767d739bb5f79dec.png)](https://gyazo.com/9d8dde83ea0cd221767d739bb5f79dec)

上段の右側に注目、`data = undefined`となっておりdataが渡ってきていないことが確認できました。

```ruby:comments_controller.rb
def update
    @comment.update!(comment_update_params)
  # 追加
    render @comment
end
```

これでうまくいくかなあ...
ダメでした。この書き方だと何やらオブジェクトそのものがdataに入っているようでした。
なのでハッシュに近いjson形式にしてデータを取り出せるようにします。

```ruby:comments_controller.rb
def update
    @comment.update!(comment_update_params)
    render json: @comment
end
```

再チャレンジ！
[![Image from Gyazo](https://i.gyazo.com/3e711cb17d79db3725feee0da138eb58.gif)](https://gyazo.com/3e711cb17d79db3725feee0da138eb58)

成功！！！

一応デバックしてdataの中身も見てみました。
[![Image from Gyazo](https://i.gyazo.com/195f5f3c7ab109c5284f9b50eb1d355d.png)](https://gyazo.com/195f5f3c7ab109c5284f9b50eb1d355d)

しっかりデータが入ってることが確認できました。

## 4.更新ボタンクリック失敗でエラーメッセージ表示

[![Image from Gyazo](https://i.gyazo.com/09e0b0266610c837aebb67d9a030e9d3.gif)](https://gyazo.com/09e0b0266610c837aebb67d9a030e9d3)

先ほどは`.done`で成功時の処理を記述しました。
失敗時には`.fail`という書き方をします。

```ruby:application.js
# .doneの処理の下に追加
.fail(function () {
  const commentError = $('#js-comment-post-error-' + commentId); # ①コメントエラー用の空タグ
  commentError.text('コメントを入力してください');                       # ②空タグにメッセージを書き換える
})
```

①こちらは前もって一意のコメントに用意しておいた空のpタグです。
下記の3行目に用意してありました。

```ruby:一意のコメント
<div id="js-comment-<%= comment.id %>">
  <p id="js-comment-label-<%= comment.id %>"><%= comment.body %></p>
  <p id="js-comment-post-error-<%= comment.id %>" class="text-danger"></p>
  <textarea style="display: none;" id="js-textarea-comment-<%= comment.id %>" class="form-control comment-post-error"><%= comment.body %></textarea>
  <div id="js-comment-button-<%= comment.id %>" style="display: none;">
    <button data-cancel-id=<%= comment.id %> type="button" class="btn btn-light comment-cancel-button">キャンセル</button>
    <button data-update-id=<%= comment.id%> type="submit" class="btn btn-success comment-update-button">更新</button>
  </div>
</div>
```

チャレンジ！
[![Image from Gyazo](https://i.gyazo.com/bab0f03ebef244b7daa801a7feb60902.gif)](https://gyazo.com/bab0f03ebef244b7daa801a7feb60902)

うまくいきました！！

## 余談 doneとfailについて

ajax関数はリクエストの成功なら`done`を、失敗なら`fail`を実行するようになっていますが、この成功失敗は何をもって判断しているのか。
これはHTTPステータスコードで判断します。
全て試したわけじゃないですが、200番台なら`done`へ、400番台なら`fail`へ処理が流れます。(500番台もいくつか`fail`へ処理が流れました。)


```ruby:comments_controller.rb
def update
    @comment.update!(comment_update_params)
    render json: @comment
end
```

記事の中で使用したこのアクション内で`update!`とエクスクラメーションをつけているのは`update`に失敗した時にfalseではなく**エラーを返してほしい**からです。
今回の場合は失敗すると422番のステータスコードが返っていたので`fail`へ処理が流れました。

【失敗時に任意のステータスコードを返すやり方】

```ruby:comments_controller.rb
def update
  if @comment.update(comment_update_params)
    render json: @comment
  else
    head :bad_request # ①ステータスコードを400番で返す
  end
end
```

ステータスコードはHTTPレスポンスのステータス行に格納されて返ってきます。
`head :hogehoge`のように記載すると`hogehoge`部分に対応したステータスコードを返すことができます。
HTTPメッセージ資料：https://itsakura.com/network-http-get-post
ステータスコード資料：https://developer.mozilla.org/ja/docs/Web/HTTP/Status


# まとめ
今回の実装は非常に苦労しました。
というのも今までデバックはそこまでしてこなかったですしHTTPを意識して実装もしてきませんでしたので。
今回そのあたりの知識が少しはついたかなと思います。

Webの基礎的な部分の勉強でわかりやすいYouTubeチャンネルがあったので初学者の方はぜひ見てみてください！
https://www.youtube.com/channel/UCTmF42qi_LeT81fGdzoCn0w