【フリマアプリ】商品詳細ページのコメント機能（第6回）〜ajax編〜

　某スクールにおいて、チーム開発で、フリーマーケットアプリを作成中であり、使用した技術について公開しています。
※初学者のため、ミスや認識違いが多々あると思いますがご了承ください。

# 商品詳細ページにコメント機能を実装しました。
全部で7回に分けて記事を投稿しています。

    |内容             |                  url   |
|:-----------------|------------------:|:-----------------------------:|
| 第１回             |       モデル、マイグレーション編 |    https://qiita.com/sho_U/items/03108801146e65d58413   |
| 第２回         |           ルーティング編 |     https://qiita.com/sho_U/items/5c829b3060be2cce919a |
| 第３回|コントローラー編|https://qiita.com/sho_U/items/8528f336cf0d470cd719|
|第４.1回|ヴュー編(一覧表示）|https://qiita.com/sho_U/items/6190562270c722956547|
|第４.2回|ヴュー編(インプットフォーム）|https://qiita.com/sho_U/items/67c2ede4fc6c605283e2|
|第５回|jquery編|https://qiita.com/sho_U/items/d72b60114f76380d05f6|
|第6回|ajax編|https://qiita.com/sho_U/items/caed9b1471e63d43dd3a|

〜コード全文〜https://qiita.com/sho_U/items/310ac5b653bdfcb99a2c


#ajax（非同期通信について）
ajaxを用いて実装する部分は、**「コメント作成」「コメント復元」**した場合となります。「コメント作成」「コメント復元」については、非同期通信を用いてデーターベースにアクセスし、受け取ったデーターを元に、画面上の表示の一部を作り替えて変更します。

##まずは**「コメント作成」**について説明したします。

###動作イメージ
![3aaeca9592fab557cf4cd0b34b041388.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/a6382371-8ca6-a14f-67ee-abbc3ffa4b94.gif)

```
1. 「コメントする」をクリックしたら、jqueryが発火
2. 入力されたデーターを非同期でサーバーに送り、json情報を受け取る。
3. 受け取った情報を元に条件をif文でふるいにかけてhtmlを形成する。
4. 作成作成したhtmlをコメント一覧に追加する。
```
という流れになります。

以下が、コメントを追加するボタンをクリックした際に、発動するプログラムとなります。

```javascript:comment.js
// ===================================
// コメント作成した場合
// ===================================
  $('.new_comment').on('submit', function(e){
    e.preventDefault()
    var formData = new FormData(this);
    var url = $(this).attr('action');
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
  .done(function(comment_data){
    var html = new_comment(comment_data);
    $(".comment_list").append(html)
    $('#comment_body').val("");
    $('.comment_list').animate({ scrollTop: $('.comment_list')[0].scrollHeight});
  })
  .fail(function() {
    alert("メッセージ送信に失敗しました");
  });
});
```
解説します

```javascript
$('.new_comment').on('submit', function(e){
 e.preventDefault()
```
onメソッドで、第一引数にsubmitイベントを指定し、フォームデーターが送信されたら発火して、まずブラウザのデフォルトの動作を停止します。

```javascript
    var formData = new FormData(this);
```
イベント発火元の情報**（"this")**を引数に**FromData**を作成し、**変数formData**に格納します。
ちなみに**FormData**とは、読み込んだブラウザ上から、さらにHttp通信リクエストを行うためのAPIである**XMLHttpRequest**を使用して、送信するためのキーと値のペアのセットを収集可能にします。

FormDataについて（参考）
https://qiita.com/akkt222/items/ce93d53e8d92a78a0694
XMLHttpRequestについて（参考）
https://ja.javascript.info/xmlhttprequest
XMLについて（参考）
https://www.cybertech.co.jp/xml/contents/xmlxmldb/serial/_xmlbeginner2.php

```javascript
var url = $(this).attr('action');
```
**attrメソッド**で、発火元フォームの**action属性の値（"/comments"）**を取得し**変数url**に格納しています。

```html
ちなみにフォームタグの属性は以下のようになっています。
<form class="new_comment" action="/comments" accept-charset="UTF-8" method="post">
```

```javascript
$.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
```
ajax通信を行います。
**url:** は、先ほどの変数urlに入っている値、つまり　"/comments"　。
**type:** でHTTPのリクエストメソッドを**POST**に指定。これにより、**commentsコントローラーのcreateアクション**を送信先に指定します。
**dataType:**　で受け取るデーターのタイプを **json**に指定します。

**processData: false**
**contentType: false**
> data:
    サーバーへ送信するデータです。 もし文字列で無ければ、クエリー文字列に変換されます。 これはGETリクエストのURLに追加されます。 この自動的に行われるプロセスを防ぎたい場合は、processDataのオプションを参照してください。

>processData: 
デフォルトでは、dataオプションにオブジェクトとして渡されるデータ(厳密に言えば、文字列以外のもの)は、 デフォルトのcontent-typeである"application/x-www-form-urlencoded"に合わせた形式でクエリー文字列へ変換されます。 もしDOMDocument、またはその他の形式のデータを送信したい場合は、このオプションをfalseに設定します。
参考：https://nodoame.net/archives/7215

>contentType:はデータ送信時のcontent-typeヘッダの値になりますが、FormDataオブジェクトの場合は適切なcontentTypeが設定されるので、同じくfalseを設定します。
参考：http://www.koikikukan.com/archives/2014/09/30-013333.php

今回は、httpメソッドは”post"であり、クエリー文字列に変換する必要はないため**false**となる。

####続いては、データーが送られてきたcommentsコントローラーのcreateアクションでの処理をみていきます。

```ruby:commnets_controller.rb
def create
  @comment = Comment.new(comment_params)
  @seller_of_item = @comment.item.seller
  if @comment.save
     respond_to do |format|
       format.json
     end
  else
    flash[:alert] = "保存できていません"
    redirect_to item_path(params[:id])
  end
end

private
  def comment_params
    params.require(:comment).permit(:comment,:item_id).merge(user_id: current_user.id)
  end
end
```
解説します。
@commnetsに、フォームから送られてきたデーターを、@seller_of_itemには出品者を格納しています。
(詳細は、第３回〜コントローラー編〜https://qiita.com/sho_U/items/8528f336cf0d470cd719)

```ruby
if @comment.save
     respond_to do |format|
       format.json
     end
```
@commnetのsaveが正常に行われた場合、**respond_to**メソッドにより、**json形式でリクエストされた場合**は、**jbuilder**を通じて、**json形式**でデーターが返されます。

```views/comments/create.json.jbuilder
json.comment @comment.comment
json.id @comment.id
json.user_nickname @comment.user.nickname
json.user_id @comment.user.id
json.created_at @comment.created_at.strftime("%Y年%m月%d日 %H時%M分")
json.item_seller @seller_of_item
```
jbuilderは

```
json.<キー> <値>
=> {"キー": "値"}
```
とすることで簡単に、json形式でデーターを作成することができます。

```javascript:comment.js
 .done(function(comment_data){
   var html = new_comment(comment_data);
```
通信が成功した場合は、返されたjsonを、**comment_data**として引数に格納します。
検証で、comment_dataの中身を確認すると

```
comment_data:
  comment: "お安く提供します。"
  created_at: "2020年05月02日 00時52分"
  id: 21
  item_seller:
    avatar: null
    birthday: "2018-06-06"
    created_at: "2020-04-04T14:55:54.000+09:00"
    email: "2@gmail.com"
    family_name: "高木"
    family_name_reading: "タカギ"
    first_name: "ぶー"
    first_name_reading: "ブー"
    id: 2
    nickname: "ブー"
    password: null
    phone_number: "08022222222"
    self_introduction: null
  updated_at: "2020-04-04T14:55:54.000+09:00"
  __proto__: Object
  user_id: 2
  user_nickname: "ブー"
```
json形式でコントローラーから送られたデーターが格納されていることがわかります。

この**comment_data**を引数に、**new_commentメソッド**を呼び出しています。
呼び出した返り値を**変数html**に格納します。

```javascript:comment.js
// ===================================
// 新規コメント表示用・自分のコメント復元用 
// ===================================
  function new_comment(comment_data){
    var HTML_content_time = 
      `
      <div class="comment_Me comment_one_block" data-index=${comment_data.id}>
        <div class="comment_content">
          ${comment_data.comment}
          <div class="comment_create_at">
            ${comment_data.created_at}
          </div>
      `
    var HTML_deleteBtn =  
        `
        <div class="comment_delete me_pre_delete" data-index=${comment_data.id}>
          <a rel="nofollow" data-method="patch" href="/comments/${comment_data.id}">削除する</a>
        </div>
        `
    var HTML_nickname =
        `
        </div>
        <div class="comment_user_name">
          ${comment_data.user_nickname}
        `
    var HTML_sellerMark =
        `
          <div class="seller_display">
          出品者
          </div>
        `
    var HTML_endDiv =
      `
        </div>
      </div> 
      `
    if (comment_data.item_seller.id == comment_data.user_id){
        // 出品者とコメントしたユーザーが等しい場合
      var html = HTML_content_time + HTML_deleteBtn + HTML_nickname + HTML_sellerMark + HTML_endDiv
    }else{
       // 出品者とコメントしたユーザーが異なる場合
      var html = HTML_content_time + HTML_nickname + HTML_endDiv
          };

    return html;
  };
```
追加するコメントを、部分ごとに分けて作成し、出品者がコメントしたのか、出品者以外がコメントしたのかでif文で場合分けして、変数htmlを組み立てています。（出品者のコメントには、**削除ボタン**と**出品者マーク**が表示される）

```javascript:comment.js
    $(".comment-list").append(html)
    $('#comment_body').val("");
    $('.comment_list').animate({ scrollTop: $('.comment_list')[0].scrollHeight});
```
帰ってきた変数htmlをコメントの一覧に追加して、コメントフォームの中身（入力された文字）を消去して、画面を新しく追加したコメントの位置までスクロールします。

これで、非同期通信を用いた**「コメント作成」機能**が実装できました！！

##最後に**「コメント復元」**について説明したします。

###動作イメージ
　　　　　　自分のコメントを復元
![865a81ee7b88e763a74a94316ed88e45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/64509c9f-4d25-bef9-1757-e372f1009b0e.gif)

　　　　　　他人のコメントを復元
![8e0e551b6c2f240e395c4048326d6d09.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/41607d6f-54cc-6610-2035-5448fa8ab691.gif)

以下が、「復元するボタン」をクリックした際に、発動するプログラムとなります。

```javascript:comment.js
// ===================================
// 復元した場合
// ===================================
$(".comment-list").on('click',".comment-restore",function(e){
  e.preventDefault()
  var index = $(this).data("index")
  var url =`/comments/${index}/restore`
  $.ajax({
    url: url,
    type: "get",
    dataType: 'json',
  })
  .done(function(comment_data){
    if (comment_data.item_seller.id == comment_data.user_id){   // 出品者とコメントユーザーが同じ場合
      var html = new_comment(comment_data);
      $(`.comment-one-block[data-index=${index}]`).replaceWith(html)
    }else{    // 出品者とコメントユーザーが異なる場合
      var html = restore_other_comment(comment_data);
      $(`.comment-one-block[data-index=${index}]`).replaceWith(html)
    }
  })
  .fail(function() {
    alert("メッセージ送信に失敗しました");
  });
});
```  

解説します。

```javascript:comment.js
$(".comment-list").on('click',".comment-restore",function(e){
  e.preventDefault()
    var index = $(this).data("index")
    var url =`/comments/${index}/restore`
```
**「復元ボタン」**がクリックされたら発火します。
発火元の**「復元ボタン」**に与えられたカスタム属性値（コメントのid)が、**変数index**に格納されます。
**変数url**には、pathの中に**index**が埋め込まれた形で格納されます。

```javascript:comment.js
    $.ajax({
    url: url,
    type: "get",
    dataType: 'json',
  })
```
**commentsコントローラー**の**updateアクション**に返却値を**json形式**でリクエストし、データーが送られます。

```ruby:commnets_controller.rb
  def restore
    @comment.update(delete_check:0)
    @seller_of_item = @comment.item.seller
    respond_to do |format|
    format.json
  end

  def set_comment
    @comment = Comment.find(params[:id])
  end
```
送られてきた**params[:id]**で、**復元すべきコメント**を特定し、**@commnet**に格納し、**delete_checkを"0"**にして**「仮削除状態」**を解除して更新する。
jbuilderを呼び出し、json形式にして返却する。

```views/comments/restore.json.jbuilder
json.comment @comment.comment
json.id @comment.id
json.user_nickname @comment.user.nickname
json.user_id @comment.user.id
json.created_at @comment.created_at.strftime("%Y年%m月%d日 %H時%M分")
json.item_seller @seller_of_item
```
```javascript:comment.js
.done(function(comment_data){
    if (comment_data.item_seller.id == comment_data.user_id){   // 出品者とコメントユーザーが同じ場合
      var html = new_comment(comment_data);
      $(`.comment-one-block[data-index=${index}]`).replaceWith(html)
    }else{    // 出品者とコメントユーザーが異なる場合
      var html = restore_other_comment(comment_data);
      $(`.comment-one-block[data-index=${index}]`).replaceWith(html)
    }
```
**「出品者自身のコメント」**と、**「その他のユーザーのコメント」**で場合分けしている。理由は、出品者自身のコメントを復元した場合は、**コメント作成時に追加するhtmlと構造が同じ**であるため、**同じコードを流用**できるため。

```javascript:comment.js
  if (comment_data.item_seller.id == comment_data.user_id){   // 出品者とコメントユーザーが同じ場合
  var html = new_comment(comment_data); //コメント作成時と同じメソッドを呼び出している。
```

```javascript:comment.js
    $(`.comment-one-block[data-index=${index}]`).replaceWith(html)
```
「復元ボタン」と同じカスタムデーター属性値を持つコメントを取得し（つまり復元ボタンが所属するコメント）、中身を**replaceWith**メソッドで返り値の**html**で置き換えている。

```javascript:comment.js
 }else{    // 出品者とコメントユーザーが異なる場合
      var html = restore_other_comment(comment_data);
      $(`.comment_one_block[data-index=${index}]`).replaceWith(html)
    }
```

```javascript:comment.js
// ===================================
// 他人のコメント復元用 
// ===================================
  function restore_other_comment(comment_data){
    var html = 
    `
    <div class="comment_Other comment_one_block" data-index=${comment_data.id}>
      <div class="comment_user_name">
      ${comment_data.user_nickname}
      </div>
    
      <div class="comment_content_other">
        ${comment_data.comment}
        <div class="comment_create_at">
          ${comment_data.created_at}
        </div>
        <div class="comment_delete other_pre_delete" data-index=${comment_data.id}>
          <a rel="nofollow" data-method="patch" href="/comments/${comment_data.id}">削除する</a>
        </div>
      </div>
    </div>
    `
  return html;
  };
```
##これで、コメント機能における全ての実装が完了しました！！