railsでコメント投稿機能を非同期化する(jQueryなし)

# 開発環境
Mac OS Catalina 10.15.7
ruby 2.6系
rails 6.0系

# 前提
同期でのコメント投稿機能は実装済みとする
JavaScriptのフレームワークは使っていません


各テーブルとアソシエーションは以下の通り

### users テーブル

| Column             | Type   | Options                   |
| ------------------ | ------ | ------------------------- |
| nickname           | string | null: false               |
| email              | string | null: false, unique: true |
| encrypted_password | string | null: false               |

### Association

- has_many :posts
- has_many :comments
- has_many :likes

### postsテーブル

| Column             | Type       | Options                         |
| ------------------ | ---------- | ------------------------------- |
| title              | string     | null: false                     |
| explanation        | text       |                                 | 
| category_id        | integer    | null: false                     |
| animal_name        | string     |                                 |
| user               | references | null: false, foreign_key: true  |

### Association

- belongs_to :user
- has_many   :comments

### commentsテーブル

| Column          | Type       | Options                        |
| --------------- | ---------- | ------------------------------ |
| user            | references | null: false, foreign_key: true |
| post            | references | null: false, foreign_key: true |
| content         | string     | null: false                    |

### Association

- belongs_to :user
- belongs_to :post

# 部分テンプレートに切り出す

まずは差し替えたい部分を切り出します。
自分の場合はコメント部分を切り出す事にしました。
切り出したものはapp/views/commentsのなかに配置しましょう。(commentsディレクトリが無ければ作ってください。)

```_comment.html.erb

# このIDは削除する時に使うもので、コメント投稿の非同期化には関係ありません。
<p id="comment_<%= comment.id %>">
  <strong><%= link_to comment.user.nickname, user_path(comment.user.id) ,class: "comment-user"%>：</strong>
  <%= comment.content %>
  <% if user_signed_in? && current_user.id == comment.user.id %>
    <span><%= link_to '[削除する]', post_comment_path(post.id, comment.id), method: :delete, class: "comment-delete", remote: true %></span>
  <% end %>
</p>
```

```show.html.erb
<div class="comment-container">
   <div class = "comment-box">
      <h2>気になった投稿にコメントしよう!</h2>
      <% if user_signed_in? %>
        <%= form_with(model: [@post, @comment], remote: true) do |form| %>
          <%= form.text_area :content, placeholder: "コメントする", rows: "2" %>
          <%= form.submit "コメントを送信する" %>
        <% end %>
      <% else %>
        <strong><p class = "alert">※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p>
        </strong>
      <% end %>
      <div class="comments" id="comments">
        <h4>＜コメント一覧＞</h4>
        <% @comments.each do |comment| %>
          # この部分を切り出しました。
          <%= render "comments/comment", post: @post, comment: comment %>
        <% end %>
      </div>
    </div>
 </div>
```

ポイントは部分テンプレート内で使う変数を渡してあげる事です。

```
# この部分のこと
post: @post, comment: comment 
```

# form_withをremote: trueにする

次にform_withのlocal: trueをremote: trueに変更します。

変更前

```show.html.erb
<%= form_with(model: [@post, @comment], local: true) do |form| %>
  <%= form.text_area :content, placeholder: "コメントする", rows: "2" %>
  <%= form.submit "コメントを送信する" %>
<% end %>
```
---
変更後

```show.html.erb
<%= form_with(model: [@post, @comment], remote: true) do |form| %>
  <%= form.text_area :content, placeholder: "コメントする", rows: "2" %>
  <%= form.submit "コメントを送信する" %>
<% end %>
```

これで、コントローラーのcreateアクションのビューの参照先が、create.js.erbに変わりました。(コントローラーはlocal: trueにするとhtml.erbのビューを、remote: trueにするとjs.erbのビューを探しに行きます。)

# コントローラーのリダイレクトの記述を削除する

せっかくremote: trueにしてもリダイレクトしてしまうので、削除しましょう。

変更前

```comments_controller.rb
class CommentsController < ApplicationController

  def create
    @comment = Comment.create(comment_params)
    redirect_to post_path(params[:post_id])
  end

  def destroy
    @comment = Comment.find(params[:id])
    @comment.destroy
    redirect_to post_path(params[:post_id])
  end

  private
  def comment_params
    params.require(:comment).permit(:content).merge(user_id: current_user.id, post_id: params[:post_id])
  end

end

```

---
変更後

```comments_controller.rb
class CommentsController < ApplicationController

  def create
    @comment = Comment.create(comment_params)
  end

  def destroy
    @comment = Comment.find(params[:id])
    @comment.destroy
  end

  private
  def comment_params
    params.require(:comment).permit(:content).merge(user_id: current_user.id, post_id: params[:post_id])
  end

end
```

# create.js.erbを編集する

commentsディレクトリにcreate.js.erbファイルを作り、以下のように編集します。

```create.js.erb
var element = document.querySelector(".comments")
element.innerHTML += '<%= j(render partial: "comments/comment", locals: {post: @comment.post, comment: @comment}) %>'
document.querySelector("#comment_content").value = ""
```

コードを説明すると、まず1行目で、コメントの一覧表示がされるcommentsクラスを持つ要素を取得して、elementという変数に代入しています。(詳しくは上記のshow.html.erbを参照してください)

その後、変数elementにinnerHTMLを使い、切り出した_comment.html.erb(コメントの中身の部分)を追加しています。
また、_comment.html.erbでは変数commentと変数postが使われているので、commentsコントローラーで変数@commentに保存したコメントを代入する記述を書き(上記comments_controller.rb参照)、create.js.erbでlocalsオプションを使って、データを渡してあげましょう。

これでコメント投稿の非同期化は完成です。

しかし、このままだと、コメントの投稿フォームに、投稿したコメントが残ってしまうので、最後にコメントの中身を削除するために、投稿フォームをIDで取得して、空にする記述を書いています。

長くなりましたが、以上です。
参考になれば幸いです。