Railsアプリに非同期通信のコメント機能を実装

# はじめに
Railsアプリに非同期通信のコメント機能を実装しました。
非同期通信は一言で言えば、画面遷移をせずにページを更新する技術です。
備忘録として手順をまとめたいと思います。

# 実装イメージ
![ezgif.com-gif-maker (8).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/772095/d4147e2f-b50e-25e2-b9d4-d869e150676d.gif)

# 開発環境

* macOS Catalina
* Ruby 2.6.5
* Ruby on Rails 5.2

# 目次
[1.モデルとアソシエーション](#1-モデルとアソシエーション)
[2.ルーティング](#2-ルーティング)
[3.コントローラー](#3-コントローラー)
[4.view](#4-view)

## 1. モデルとアソシエーション
モデル構成とアソシエーションはよくある構成です。
前提としてuserとpostモデルは作成済みとします。
![スクリーンショット 2021-04-16 22.54.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/772095/035aa8e5-5c1f-b95f-6ec1-27646d553a4b.png)

### commentモデル作成

```php:
$ rails g model comment user:references post:references content:string
$ rails g migrate
```

### アソシエーション

```php:user.rb
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
```

```post.rb
  belongs_to :user
  has_many :comments, dependent: :destroy
```

```php:comment.rb
  belongs_to :user
  belongs_to :post
 
  # バリデーション（カラの入力を無効に）
　validates :content, presence: true
```

## 2. ルーティング
コメントは投稿に紐付いているのでルーティングはネストさせて記述します。
ネストさせることでアソシエーション先のレコード（今回で言えば、投稿に紐づくコメント）のidをparamsに追加してコントローラーにわたすことができるようになります。

```routes.rb
resources :posts, only: [:index, :new, :create, :show, :destroy] do
  resources :comments, only: [:create, :destroy]
end
```

## 3. コントローラー
まずはcommentのコントローラーを作成します。

```php:
$ rails g controller comments
```
次にcreateとdestroyアクションを定義します。

### comments_controller.rb

```php:comments_controller.rb
class CommentsController < ApplicationController

  def create
    @comment = Comment.create(comment_params)
    respond_to do |format|
      if @comment.save
        format.html { redirect_back(fallback_location: root_path) } # 前のページに遷移
        format.js  # create.js.erbが呼び出される
      else
        format.html { redirect_back(fallback_location: root_path) } # 前のページに遷移
      end
    end
  end

  def destroy
    @post = Post.find(params[:post_id])
    @comment = current_user.comments.find_by(post_id: @post.id)
    @comment.destroy
    redirect_back(fallback_location: root_path)
  end

  private
  def comment_params
    params.require(:comment).permit(:content).merge(user_id: current_user.id, post_id: params[:post_id])
  end
  
end
```
 **Comment.create(comment_params)**
・comment_paramsではmergeメソッドでuser_idとpost_idをcommentテーブルのレコードに格納します。

**respond_to do |format|**
・処理の結果をHTML形式で返すかJS形式で返すかを分岐させます。
・今回はコメントが保存されたらJS形式で返すように設定します。**format.js**と記述することで**create.js.erb**というファイルを返します。

**redirect_back(fallback_location: root_path)**
・もしJS形式で返せなかった場合は同期通信でコメントを作成します。その際redirect_backでコメント作成ページにとどまることができます。fallback_location: root_pathはエラーが起きた際にroot_pathに遷移するという記述です。

### posts_controller.rb
viewで反映させるためにposts_controller.rbを編集します。

```php:posts_controller.rb
 def show
  @post = Post.find(params[:id])
  @comment = Comment.new
  @comments = @post.comments.all
 end
```

## 4. View
非同期通信をさせるために**create.js.erb**と**_comment.html.erb**という部分テンプレートを作成します。
一例ですがディレクトリ構成は以下のようになります。

```php:
views
|-posts
|    |-show.html.erb
|-comments
     |-_comment.html.erb
     |-create.js.erb

```

投稿詳細ページは以下のようになります。

```php:posts/show.html.erb
 <% if @comments %>
    <div class="commentOutline">
      <%= render partial: "modules/comment", locals: { comments: @comments }%>
    </div>
  <% else %>
    <p>コメントはまだありません</p>
  <% end %>

  <div class="bottomInput">
    <%= form_with(model: [@post, @comment], id: "new-comment") do |f| %>
      <%= f.text_field :content, class: "inputComment" %>
      <%= f.submit "コメントする", class: "submitComment" %>
    <% end %>
  </div>
```
renderメソッドで**_comment.html.erb**を呼び出します。更にlocalsオプションでposts_controller.rbで定義した@commnetsの変数をcommentsとして部分テンプレート内で使用できるようにします。
form_withはデフォルトでremote: tureになっているのでこれでJS形式でレスポンスすることができます。

部分テンプレートは以下の通りです。

```php:comments/_comment.html.erb
<% comments.each do |comment| %>
  <li>
    <div class="topPosition">
      <%= link_to user_path(comment.user.id), class: "commentUserLink" do %>
        <% if comment.user.icon? %>
          <%= image_tag comment.user.icon.url, class: "commentUserIcon"%>
        <% else %>
          <i class="fas fa-user-circle"></i>
        <% end %>
        <p class="commentUserName"><%= comment.user.nickname %></p>
      <% end%>
      <% if comment.user_id == current_user.id %>
        <%= link_to post_comment_path(comment.post_id, comment.id), method: :delete, class: "deleteCommentLink" do %>
          <i class="fas fa-trash"></i>
        <% end %>
      <% end %>
    </div>
    <div class="bottomPosition">
      <p class="commentContent"><%= comment.content %></p>
      <p class="commentDatetime"><%= comment.created_at.strftime('%Y/%m/%d') %></p>
    </div>
  </li>
<% end %>
```
最後にcreate.js.erbを編集します。

```php:comments/create.js.erb
$(".commentsArea").html("<%= j(render 'comments/comment', { comments: @comment.post.comments }) %>")
$(".inputComment").val('');
```
1行目でコメントが作成されたらcommentsAreaに部分テンプレートの内容を更新するという記述をしています。
2行目はコメントを入力するinputエリアの値をリセットする記述です。なお記述を簡略にするためにjQueryで記述しています。

以上で非同期でのコメント機能が実装できていると思います！