コメント機能のAjax処理

コメント投稿、コメント削除機能をAjaxを用いて動的に作ってみました。
ページ遷移するより圧倒的に使いやすくなります。

初めて実装してみたので、Ajaxについての内容と実装内容、詰まった際に行ったデバッグ方法をまとめておきます。

環境：
Rails 6.0.3.2

## Ajaxについて
##### Ajax（非同期通信）を使用
Webブラウザ上でJavaScriptがWebサーバーと通信を行い、一部の情報をリクエスト＋変更箇所だけを取得して反映できる。
　➡︎ 送信データ量が少なくなるためサーバーへの負担が抑えられる。
　➡︎ 表示スピードが圧倒的に早くなる。

##### 同期通信を使用
クライアントとサーバーが交互に処理を行い、同調して通信を行うこと。
webブラウザがリクエストを通信し、webサーバーが作成したHTMLファイルをレスポンスとして返し、webブラウザがそれを受け取って表示する。
　➡︎ 送信データ量が多くなるためサーバーへ負担をかける。
　➡︎ HTMLファイルを受け取ってから表示の処理を行うため、ページの更新に時間がかかる。

## Ajaxの実装、未実装の違い
何がどう変わるのかイメージしたい方は、[こちら](https://pikawaka.com/rails/remote-true)を参考にするといいと思います。
（独学で初めて実装した際に自身も気になったので載せておきます）

## 1.今回の完成イメージ

![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655853/2f01b139-e363-d993-5a0e-cf3040e48de0.gif)


## 2.スキーマ（コメントテーブルの作成）
```　db/schema.rb
  create_table "comments", force: :cascade do |t|
    t.integer "tournament_id"
    t.integer "post_id"
    t.integer "user_id"
    t.text "body"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

```

どのトーナメントかを格納するための「tournament_id」
どの投稿に対するコメントなのかを格納するために「post_id」
誰が投稿したかを格納するために「user_id」
コメント内容を格納するための「body」カラムを作成。

## 3.モデル（アソシエーションの記述）
【comment モデル】

```　app/models/comment.rb
  belongs_to :user
  belongs_to :post
  belongs_to :tournament
  validates :body, presence: true　（コメント内容を必須にするバリデーションを設定）
```
一対多の関係になっているので、`berongs_to`でのアソシエーションになる。
・トーナメントにはたくさんのコメントを持つ
・ユーザーはたくさんのコメントを持つ
・投稿はたくさんのコメントを持つ


【tournament モデル】

```　app/models/tournament.rb
  belongs_to :user
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
```
トーナメントのアソシエーションはこのようにします。

・トーナメント作成ユーザーは1人
・トーナメントはたくさんの投稿を持つ
・トーナメントはたくさんのコメントを持つ
`dependent: :destroy` →（トーナメントが削除された時に、同時に投稿、コメントも消去する）

【post モデル】

```　app/models/post.rb
  belongs_to :user
  belongs_to :tournament
  has_many :comments, dependent: :destroy
```
投稿のアソシエーションはこのようにします。
・投稿するユーザーは１人
・投稿先のトーナメントは1つ
・投稿はたくさんのコメントを持つ
（投稿が削除された時に、同時にコメントも消去する）

【user モデル】

```　app/models/user.rb
  has_many :tournaments
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
```
ユーザーのアソシエーションはこのようにします。
・ユーザーはたくさんのトーナメントを持つ
・ユーザーはたくさんの投稿を持つ
・ユーザーはたくさんのコメントを持つ
（ユーザーが削除された時に、同時に投稿、コメントも消去する）


## 4.ルーティング

```　config/routes.rb
  resources :tournaments do
    resources :posts do
      resources :comments, only: [:create, :destroy]
    end
  end
```
コメントが、どの投稿でどのトーナメントであるかを識別するためにネストして記述します。


## 5.コメントコントローラの記述

```　app/controllers/comments_controller.rb
  def create
    @tournament = Tournament.find(params[:tournament_id])　※①※
    @post = Post.find(params[:post_id])　※②※
    @comment = current_user.comments.build(comment_params)　※③※
    @comment.save!
  end

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

  private

  def comment_params
    params.require(:comment).permit(:body).merge(post_id: params[:post_id], tournament_id: params[:tournament_id])
  end
```
createアクション：
※①※：コメントをする対象のトーナメント(tournament)のインスタンスを作成。
※②※：コメントをする対象の投稿(post)のインスタンスを作成。
※③※：コメントのインスタンスを作成。
##### 注：保存後に`redirect_to `を記述すると画面遷移が起こってしまい、Ajax処理にはなりません。ここではcreateアクションと同じ名前の`create.js.erb`ファイルを探しにいってくれるようにjsファイルを作成する必要があります。（こちらは後から作成します）

destroyアクション：
※④※：削除する対象のコメントインスタンス作成。
※⑤※：対象のコメント削除
(ここも同様に`redirect_to `は記述しない)


## 6.Postコントローラの記述

```　app/controllers/posts_controller.rb
  def show
    @tournament = Tournament.find(params[:tournament_id])
    @post = Post.find(params[:id])
    @comment = Comment.new　※①※
    @comments = @post.comments　※②※
  end
```
※①※：インスタンスを作成。（入力フォームで使用します）
※②※：コメントのデータを格納。（コメント一覧表示で使用します）


## 7.投稿画面のビュー

```　app/views/posts/show.html.erb
  <div>
    <h4>コメント</h4>
    <div id="comments_area">　※①※
      <%= render partial: 'comments/index', locals: { comments: @comments, tournament: @tournament } %>
    </div>  
    <% if user_signed_in? %>
      <%= render partial: 'comments/form', locals: { comment: @comment, post: @post } %>
    <% end %>
  </div>
```
※①※：`"comments_area"`のidを部分的に更新したい箇所として、このdiv内をAjaxで書き換える。
comments/indexのパーシャル内で使うローカル変数comment、tournamentは`locals: `として@comment、@tournamentを記述し変数の受け渡しをします。
comments/formも同様に受け渡します。


## 8.部分テンプレートのビュー
views配下に`comments`ディレクトリを作成しその中にrenderで読み込む`_index.html.erb`と`_form.html.erb`を作成します。
部分テンプレートなのでファイル名の先頭に`_`を忘れないように。

_index.html.erb：

```　app/views/comments/_index.html.erb
  <% comments.each do |comment| %>
    <% unless comment.id.nil? %>
      <p><%= link_to "#{comment.user.nickname}さん", user_path(comment.user.id) %></p>
      <p>コメント：<%= comment.body %></p>
      <% if comment.user == current_user %>
        <p><%= link_to "削除", tournament_post_comment_path(comment.tournament_id, comment.post_id, comment.id), method: :delete, remote: true %></p>
      <% end %>
    <% end %>
  <% end %>
```

・show.html.erbにて変数を渡してあるので`comments`に対してeach文で取り出す。

・削除リンクの記述では、トーナメントid、投稿id、コメントidを渡す必要があります。

・コントローラーでjsファイルを探しにいってもらいたいので`remote: true`を記述します。

_form.html.erb：

```　app/views/comments/_form.html.erb
  <%= form_with model: [@tournament, post, comment] do |f| %>
    <div>
      <%= f.text_area :body %>
    </div>
    <div class="actions">
      <%= f.submit "コメントをする" %>
    </div>
  <% end %>
```

・どのトーナメントで、どの投稿に紐づいたコメントかを生成するため`model: [@tournament, post, comment]`とします。

・`form_with`は、デフォルトで非同期通信をするように設定されているので、明示的に`remote: true`を記述する必要はないようです。


## 9.jsファイルの作成
createアクションに対して同じ名前のcreate.js.erbファイルを作成：

```　app/views/comments/create.js.erb
  $("#comments_area").html("<%= escape_javascript(render 'index', { comments: @comment.post.comments }) %>")
  $("textarea").val('')
```

書き換えたい箇所`$("#comments_area")`を指定して、`render 'index'`によって部分テンプレート（_index.html.erb）の内容を書き換えています。
`comments: @comment.post.comments`では、ローカル変数を commentsとして@comment.post.commentsを渡しています。
` $("textarea").val('')`では、書き換えた後のコメント入力欄を空にするために初期化を記述しています。

同様にdestroy.js.erbファイルを作成：

```　app/views/comments/destroy.js.erb
  $("#comments_area").html("<%= escape_javascript(render 'index', { comments: @comment.post.comments }) %>");
```


## 10.完成
これで、Ajaxを使ったコメント投稿・削除機能が実装できたはず！


## 追記

今回はコメントコントローラ内での保存処理後は、同じ名前のjsファイルに飛ぶ。ということを説明したかったので`create.js.erb`と`destroy.js.erb`として分けて行っております。

（create.js.erbとdestroy.js.erbの内容がほぼ同じなので1つのjsファイルとして保存後に`render`で呼び出す方がjsファイルがまとまっていて良いのかもしれません。。）

(例)　jsファイル名を`index.js.erb`とした時

```　app/controllers/comments_controller.rb
  def create
    @tournament = Tournament.find(params[:tournament_id])
    @post = Post.find(params[:post_id])
    @comment = current_user.comments.build(comment_params)
    @comment.save!
    render :index    ←※変更点※
  end

  def destroy
    @comment = Comment.find(params[:id])
    @comment.destroy
    render :index    ←※変更点※
  end

```


```　app/views/comments/index.js.erb
  $("#comments_area").html("<%= escape_javascript(render 'index', { comments: @comment.post.comments }) %>")
  $("textarea").val('')
```

## エラー時は検証ツールも使う

##### 【 リロードしないと削除されない.. 】
確認事項：
①エラーログを見る
②どこまで処理ができていてどこから停止しているかを理解する
③`binding.pry`を使っての確認
上記はもちろんですが、今回は

ブラウザ側の`検証ツール`を使ってエラーの原因を調べました。

①投稿ボタンを押した後で停止中に「検証」をかけます。
![スクリーンショット 2020-11-27 14.45.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655853/92040b9a-53ab-5465-0ba9-18748ce67e8d.png)

②`Network`からエラーの確認
![スクリーンショット 2020-11-27 14.51.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655853/6834e113-d8b3-8c32-a506-85407d10b47d.png)

③エラー内容
![スクリーンショット 2020-11-27 14.56.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655853/10f6a278-a40d-3716-5cd0-57cb9ddc46d5.png)

・_index.html.erbの10行目が悪いんだな。
・undefined method が出ている..pathに問題がありそうだ。。

と気付けました。（Railsのログにも「pathが悪いぞ！」とちゃんと表示されていますが。）
エラーの例としては簡単な内容でしたが、なかなか気付けない時は検証からも調べることも大切ですね。

# まとめ
最後まで読んでいただきありがとうございます！！
間違いや効率的なコードの書き方があればご指摘ください！！

今回も少し成長できたと思ってどんどんチャレンジしていきます。
お疲れ様でした！