[Rails]非同期（Ajax）でのコメント機能実装

## この記事の目的
非同期処理（ajax）でのコメント投稿機能を学んだのでアウトプットします
## 目標
[![Image from Gyazo](https://i.gyazo.com/13b13c9847585f462000a341b671e8b8.gif)](https://gyazo.com/13b13c9847585f462000a341b671e8b8)
## 要件
コメント投稿は非同期で行う
## 環境
・Ruby 2.7.1

・Rails 5.2.6
## 前提
・UserとPostは作成済み

・bootstrapとjquery-railsが入っている

## 設計
ER図
[![Image from Gyazo](https://i.gyazo.com/919c4a7bc8f4e5e4c7ff50fb2efcdab1.png)](https://gyazo.com/919c4a7bc8f4e5e4c7ff50fb2efcdab1)

<br/>

ルーティング

```
POST  /posts/:post_id/comments  comments#create
GET  /comments/:id/edit  comments#edit
PATCH  /comments/:id  comments#update
DELETE  /comments/:id  comments#destroy 
```
※ `post_id`が必要なのはコメントが`create`される場合だけであり、一度コメントが作成されればそれらコメントは一意の`id`を持つので、`edit`,`update`,`destroy`の場合はコメントの自身の`id`のみで特定可能である。   
よって、このようなルーティングにする。（`shallow: true`で実現可能）  
このようなshallowルーティングのメリットは、shallowを使わない場合と比較してルーティングがスッキリするという点である。

## 実装
### データベース

`$ rails g model Comment body:text user:references post:references`実行

マイグレーションファイル編集（任意）

`$ rails db:migrate`実行

<br/>

### モデル

<br/>

```ruby:models/comment.rb

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :post

  validates :body, presence: true, length: { maximum: 1000 }
end
```
```ruby:models/user.rb

has_many :comments, dependent: :destroy
```
```ruby:models/post.rb

has_many :comments, dependent: :destroy
```
※ `has_many`,`belongs_to`はメソッドを作るためのメソッド。

※ Userモデルに`has_many :comments`を記述することで、Userモデルのインスタンスは`.commentsメソッド`を使い、自身の`id`に対応するCommentモデルの`user_id`を通じて、コメント情報にアクセスできるようになる。Postも同様。

※ 逆に、Commentモデルに`belongs_to :user`を記述することで、Commentモデルのインスタンスは.userメソッドを使い、自身のuser_idに対応するUserモデルの`id`を通じて、ユーザー情報にアクセスできるようになる。`belongs_to :post`でも同様。

<br/>

### ルーティング

<br/>

```ruby:routes.rb

resources :posts, shallow: true do
    resources :comments
end
```
※ 上述したルーティング設計に合わせるため、shallowルーティングを使用している  
（参考：[Railsの”shallow（浅い）”ルーティングを理解する](https://qiita.com/tanutanu/items/a245f7691c77b56d4cd3)）

<br/>

### コントローラー
```ruby:comments_controller.rb
class CommentsController < ApplicationController
  
  def create
    @comment = current_user.comments.build(comment_params)
    @comment.save
  end

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

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

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

  private

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

  def comment_update_params
    params.require(:comment).permit(:body)
  end
end
```

※ `current_user.comments.find`を`Comment.find`にしてしまうと、コメントしたユーザー以外のユーザーがURL直打ちなどによって他人のコメントを編集できてしまうようになるので注意。

※ ユーザから受け取った情報としてparamsにはないけれども、レコード作成時に追加したい値がある場合は`mergeメソッド`で含めることができる（今回のpost_id）。  
`mergeメソッド`は、ハッシュ同士を統合できるメソッド。

```ruby:例
x = {age: "25", email: "tarou@example.com"}
{name: "太郎"}.merge(x)  
#=> {name: "太郎", age: "25", email: "tarou@example.com"}
```
（参考）   
[【初学者向け】Rails mergeメソッド](https://qiita.com/zenfumi/items/694edc863527f91c8976)  
[mergeメソッドについて改めて理解を深めた](https://qiita.com/TerToEer_sho/items/c445bad0a9726030d24c)

<br/>

※ `buildメソッド`は`newメソッド`と同じ。慣習的に、関連するモデルを生成するときはbuildを使う（参考：
[railsのnewとbuildの違い](https://qiita.com/sukechansan/items/6bae532b4f678fdcf87d)）


※ createアクションの処理が終わると、views/comments/ディレクトリ配下にcreate.html.erbまたはcreate.js.erbを探しに行く。edit,update,destroyも同様。

<br/>

### view

#### jsの記述
```eruby:views/comments/create.js.erb

<% if @comment.errors.present? %>
   alert("<%= @comment.errors.full_messages.join('\n') %>");
<% else %>
   $('.comments-box').prepend("<%= escape_javascript(render('comments/comment', comment: @comment) ) %>");
   $('.input-comment-body').val('');
<% end %>
```
※ slimならRubyのコードを囲む`<%=...%>`は`#{...}`となる  

※ `escape_javascript`は`j`と省略して書くこともできる  

※ 最初の`<% if @comment.errors.present? %>`の部分は、ユーザーのコメントがバリデーションに引っかかった場合にエラー文を表示する処理。  
・`<% else %>`の１行目の部分は、htmlのcomments-boxクラスで囲まれた部分にrender('comments/comment', comment: @comment) をぶちこんでいる。

__イメージ__

```html
<div class="comments-box">

   <li>コメント１</li>
   <li>コメント２</li>
   <li>コメント３</li>

   <li>新規コメント</li> ⇦ こいつがjsで追加されるイメージ

</div>
```
<br/>

__検証ツールで実際の挙動を確認__

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

※ コメントを投稿したとき、画面遷移はせずに`<div id="comment-29">...</div>`だけが追加されていることがわかる

<br/>

#### htmlの記述（投稿されたコメントの表示画面）

```eruby:views/posts/show.html.erb（postの詳細画面）
・
・
・
<hr>
      / コメント一覧
      <%= render 'comments/comments', comments: @comments %>

    <hr class="m-0">
       <div class="post-comment p-3">
         <%= render 'comments/form', post: @post, comment: @comment %>
       </div>
     </hr>
</hr>
```
```eruby:views/comments/_comments.html.erb

<div class="comments-box">
   <%= render comments %>
</div>
```
※ commentsは@comments。つまり`render @comments`で_comment.html.erbに飛ばしているのと同じ（each文省略するやつ）

```eruby:views/comments/_comment.html.erb
（ @comments.each do |comment| が省略されている ）
(<div class="comments-box">)
・
・
・

 / コメント一覧のうちのコメントひとつひとつ（|comment|）
  <div class="col-9">
    <span class="font-weight-bold.pr-1">
          <%= comment.user.username %>
    </span>
        <%= comment.body %>
  </div>

(</div>)
```

<br/>

#### htmlの記述（コメントの入力フォーム）
```eruby:views/comments/_form.html.erb

<%= form_with model: [post, comment], class: 'd-flex mb-0 flex-nowrap justify-content-between', remote: true do |f| %>
  <%= f.text_field :body, class: 'form-control input-comment-body', placeholder: 'コメント' %>
  <%= f.submit '投稿', class: 'btn btn-primary btn-raised' %>
<% end %>
```
※ form_withはデフォルトでremote: trueなので書く必要はないが、`local: true`ではないことを強調するために`remote: true`を明示。   
ちなみに`link_to`の場合は`remote: true`を必ず書かなければいけない。

※ ルーティングをネストさせているので`model: [post, comment]`と２つ書かなければいけない

※ もし`form_with model: comment`にしたら、`/comments`に`POST`リクエストが送られるが、ルーティングの`POST  /posts/:post_id/comments  comments#create`にマッチしないため、`post_id`が不存在ということでエラーが起きる。

※ edit, update, destroyについてもcreate同様にviewを作っていく

<br/>

ちなみに、検証ツールを見ればTypeが`xhr`になっており、form_withの`remote: true`が非同期処理をしていることがわかる。

>XHR(XMLHttpRequest)とは、JavaScriptなどのウェブブラウザ搭載のスクリプト言語でサーバとのHTTP通信を行うための、組み込みオブジェクト（API）である。
すでに読み込んだページからさらにHTTPリクエストを発することができ、ページ遷移することなしにデータを送受信できるAjaxの基幹技術である。（[Wikipedia引用](https://ja.wikipedia.org/wiki/XMLHttpRequest)）

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