【Rails】コメント投稿機能 (Ajax通信)

個人アプリへコメント機能の実装を行ったので、実装方法を備忘録を兼ねて投稿します。

## 環境
ruby 2.7.1p83
rails 6.0.3.4

## 実装準備
モデルとコントローラーを作成します。

```shell:terminal
$ rails g model comment
```
```shell:terminal
$ rails g controller comments
```
ルーティングを設定します。

```ruby:route.rb
resources :tickets do
  resources :comments, only: [:create, :destroy]
end
```

## Viewの作成
コメントの表示や投稿を行うviewをコメント機能をつけたいviewに追加します。
筆者の場合はチケット詳細画面にコメントを投稿できる機能をつけたかったので、tickets/show.html.erbファイル内に記載しています。
※ Userモデルと関連付けを行っていなかったので、投稿者名や投稿日が固定になっています。

```erb:tickets/show.html.erb
# コメント部分以外は省略します
<div class="comment-wrapper">
  <div class="posts">
#　コメントが投稿されているときとされていないときで処理を分けています。
    <% if @ticket.comments.blank? %>
      <p>コメントが投稿されていません。</p>
    <% else %>
# コメントを一つ一つ取り出して表示しています。
      <% @comments.each do |comment| %>
      　<div class="post">
          <div class="post-info">
            <%= image_tag "avatar.jpg", alt: "コメント投稿者のプロフィール画像", size: "35", class: "icon" %>
            <div class="post-user">
              さかい
            </div>
            <div class="post-date">
              2021年3月15日
            </div>
          </div>
          <div class="content">
            <%= comment.content %>
          </div>
        </div>
      <% end %>
    <% end %>
  </div>

  <div class="form form-group">
    <%= form_with(model: [@ticket, @comment], id: "new_comment") do |f| %>
      <%= f.text_field :content, placeholder: "コメントを入力してください。", class: "form-control" %>
      <%= f.hidden_field :ticket_id, value: @ticket.id %>
      <%= f.button type: "submit", class: "submit-btn" do %>
        <i class="far fa-paper-plane"></i>
      <% end %>
    <% end %>
  </div>
</div>
```

完成画面のイメージ画像です。好みでスタイリングしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583698/d5d463af-b787-0f69-b5a4-0f4a3892199b.png)

ビューができたら、javascriptでコントローラーにAjax通信でデータを送信する処理を書きます。

## JavaScirptで処理を記載
viewが書けたらJavaScriptを書いていきます。少し長いので、①〜③に分け、数字は実行順序を示しています。

```javascript:detail.js
$(function(){
③---------------------------------------------------------------------------
  function buildHTML(comment){
    const html = `<div class="post">
                    <div class="post-info">
                      <img alt="コメント投稿者のプロフィール画像" class="icon" src="/assets/avatar.jpg" width="35" height="35">
                      <div class="post-user">
                        さかい
                      </div>
                      <div class="post-date">
                        2021年3月15日
                      </div>
                    </div>
                    <div class="content">
                      ${comment.content}
                    </div>
                  </div>`
    return html;
  }
------------------------------------------------------------------------
①-----------------------------------------------------------------------
  $('#new_comment').on('submit', function(e){
// (a) デフォルトの送信処理を止めます 
    e.preventDefault();
// (b) FormDataオブジェクトにフォーム内容を挿入します。
    const formData = new FormData(this);
// (c) 送信先のURLを定義します。
    const url = $(this).attr('action');
    $.ajax({
      url: url,
      type: 'POST',
      dataType: 'json',
      data: formData,
      processData: false,
      contentType: false
    })
-----------------------------------------------------------------------
②---------------------------------------------------------------------
// コントローラーでの処理を終えた後、doneメソッドが実行されます。
// 引数のcommentには返されたjsonデータが入っています。
    .done(function(comment){
// 一番上に定義したbuildHTML関数を実行し、返り値をcommentHTMLの変数に入れます。
      const commentHTML = buildHTML(comment);
      $('.posts').append(commentHTML);
// コメント投稿後はフォームの中身が空になってほしいので、その処理を書きます。
      document.getElementById("comment_content").value = "";
// 「まだコメントが投稿されていません」メッセージがある際は消したいので、その処理を書きます。
      document.getElementById("empty-message").remove();
    });
  });
})
------------------------------------------------------------------------
```
フォームが送信された際に①から関数が実行されます。
(viewのところでは省略しましたが、form_withに対してidを付与しています。)
デフォルトの送信処理は止めたいので、引数にeを入れて、prevent.Defaultメソッドを実行します。(a)
ここでのthisはform情報になるので、thisを利用してFormDataオブジェクトにフォーム情報を挿入します。(b) 
FormDataオブジェクトに関しては説明を割愛します。
https://developer.mozilla.org/ja/docs/Web/Guide/Using_FormData_Objects

次に、attrメソッドを使って、formの送信先のURLを取得します。(c) action属性はformの送信先のURLを示す属性です。

①までの処理を終えると、コントローラに処理が移りますので、ここで一旦コントローラの処理の説明に入ります。

## コントローラーでの保存処理

```ruby:comments_controller.rb
  def create
    @comment = Comment.new(comment_params)
    @comment.save!
    respond_to do |format|
      format.html
      format.json
    end
  end

  private
  def comment_params
    params.require(:comment).permit(:id, :content, :user_id, :ticket_id, :created_at, :updated_at)
  end
```
### format.jsonについて
```
format.json　{ render json: @book }
```
のようにブロックでrenderメソッドを明示することで、任意のオブジェクトを返しますが、今回のようにformat.jsonのあとに何も指定しない場合、規約に合致するテンプレートを探索します。Jbuilderがインストールされている場合は、コントローラ名/create.json.jbuilderのファイルを探索します。(Rails6.0ではデフォルトでインストールされています。)
今回はJbuilderを使ってjsonデータを作成するので、format.jsonの後には何も記載しません。

さて、コントローラからJbuilderへ処理が移りますので、Jbuilderの説明に入ります。


## Jsonデータを作成
Jbuilderのファイルは自動では生成されないので自分で作ります。
コントローラはフォルダ名やファイル名を参照してファイルを探しに行きますので、必ずviews/comments/create.json.jbuilderのようにコントローラー名のフォルダを作り、その中にcreate.json.jbuilderのファイルを作成します。

```javascript:views/commments/create.json.jbuilder
json.extract! @comment, :id, :content, :created_at, :updated_at
```

json.extract!を使ってjsonデータを作成します。jbuilderの書き方に関しては割愛します。
https://github.com/rails/jbuilder
https://opiyotan.hatenablog.com/entry/rails-gem-jbuilder

ここまででコントローラで書き込んだコメントを保存し、jsonデータを取得するところまで来ました。ここで制御がjavascriptへ戻ります。
## コメント保存後の処理
だいぶ上の方に行ってしまったので、再度detail.jsの②の部分を載せます。

```javascript:detail.js
②---------------------------------------------------------------------
// (d) コントローラーでの処理を終えた後、doneメソッドが実行されます。
// 引数のcommentには返されたjsonデータが入っています。
    .done(function(comment){
// (e) 一番上に定義したbuildHTML関数を実行し、返り値をcommentHTMLの変数に入れます。
      const commentHTML = buildHTML(comment);
      $('.posts').append(commentHTML);
// コメント投稿後はフォームの中身が空になってほしいので、その処理を書きます。
      document.getElementById("comment_content").value = "";
// 「まだコメントが投稿されていません」メッセージがある際は消したいので、その処理を書きます。
      document.getElementById("empty-message").remove();
    });
  });
})
------------------------------------------------------------------------
```
コントローラでの処理を終えると、②からのdoneメソッドが実行されます。doneメソッドはAjax通信に成功した際に実行されるメソッドです。そして、そこで実行される関数の引数にはjbuilderで作ったjsonデータが入っています。このデータを使ってコメントのviewを作り、挿入していきます。(d)
コメントのviewは、もう忘れていると思いますが一番上の③で定義したbuildHTML関数を使って作成します。なお、この関数はdoneメソッドの下に書いてしまうと、実行段階で読み込まれていないためNoMethodErrorになります。そのため、doneメソッドより前で定義しています。このbuildHTML関数の引数にcommentを入れて実行し、返ってきたHTMLを変数commentHTMLに入れます。(e)
後はこれを挿入したいところにappendして終わりです。

少し長くなりましたが、これで非同期通信でコメントが投稿できるようになりました。
同じような初心者の方にお役に立てればうれしいです。