Ajaxを用いた動的なコメント投稿・削除機能の実装で学ぶRuby on Rails

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

ネット上に自分の求めていたジャストの記事が無かったので、今回の実装をまとめてみました！

個人ブログに同様の内容を書いておりましたが、技術的な内容はQiitaに集約することにしたので、こちらにも投稿します。
[（参考）個人ブログ](http://ysk-pro.hatenablog.com/entry/2018/02/10/101739)

投稿ページの実装、JQueryの読み込みはできている前提です。

※ バージョンは、Ruby：2.3.0、Rails：5.1.4です。

# スキーマ（コメントテーブル）

```ruby:config/db/schema.rb
  create_table "comments", force: :cascade do |t|
    t.text "content"
    t.integer "post_id"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
```
どの投稿に対するコメントであるかを格納するために「post_id」、誰の投稿であるかを格納するために「user_id」カラムの作成が必要です。

# モデル（アソシエーション設定）
```ruby:app/models/comment.rb
  belongs_to :user
  belongs_to :post
  validates :content, presence: true
```
ユーザーはたくさんのコメントを持てる、投稿はたくさんのコメントは持てると、一対他（ユーザー・投稿：一、コメント：他）の関係になっているので、コメントのモデルは「berongs_to」でのアソシエーションになります。

コメントの内容が無いとコメントの意味が無いので、コメント内容必須のバリデーションを設定しています。

```ruby:app/models/post.rb
  belongs_to :user
  has_many :comments, dependent: :destroy
```
ユーザーはたくさんの投稿を持てる、投稿はたくさんのコメントを持てる、の関係なので投稿のアソシエーションはこのようになります。

「dependent: :destroy」は、この場合「投稿が削除された時に、同時にコメントも消去する」という意味です。
コメントだけ残ってしまっても意味が無いので記載が必要です。

```ruby:app/models/user.rb
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
```
同様の考え方です。

# ルーティング
```ruby:config/routes.rb
  resources :posts do
    resources :comments
  end
```
コメントがどの投稿へのものであるかを識別するために、ルーティングのURLに投稿のIDを含める必要があります。
具体的には「/post/12/comment/」といったURLになります。（ネストする、と言います。）
12がpost_idです。
[（参考）Rails のルーティング | Rails ガイド](https://railsguides.jp/routing.html#%E3%83%8D%E3%82%B9%E3%83%88%E3%81%97%E3%81%9F%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9)

# コメントコントローラー
```ruby:app/controllers/comment.rb
  def create
    @post = Post.find(params[:post_id]) #①
    @comment = @post.comments.build(comment_params) #②
    @comment.user_id = current_user.id #③
    if @comment.save
      render :index #④
    end
  end

  def destroy
    @comment = Comment.find(params[:id]) #⑤
    if @comment.destroy
      render :index #⑥
    end
  end

  private
    def comment_params
      params.require(:comment).permit(:comment_content, :post_id, :user_id)
    end
```
createアクション：
 #①：コメントをする対象の投稿(post)のインスタンスを作成します。

 #②：「.build」を使うことで、@postのidをpost_idに含んだ形でcommentインスタンスを作成します。
「.new」で普通にインスタンスを作成して、次の行でpost_idを入れても同じです。
[（参考）build - リファレンス - - Railsドキュメント](http://railsdoc.com/references/build)

 #③：現在のuserのidを入れます。

 #④：保存がされると、render :indexによって「app/views/comments/index.js.erb」を探しにいきます。
「form_with」でフォームを送信した時は、デフォルトでjsファイルを探しにいく設定になっています。
htmlファイルを探しにいってほしい場合には、form_withの後に「local: true」と記載する必要があります。
[（参考：form_withについて）rails-ujs と form_with の使い方 - ボクココ](http://www.bokukoko.info/entry/2017/12/22/194327)
[（参考：renderについて）レイアウトとレンダリング | Rails ガイド](https://railsguides.jp/layouts_and_rendering.html#render%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B)

destroyアクション：
 #⑤：削除する対象のコメントインスタンスを探します。

 #⑥：削除がされると、「index.js.erb」を探しにいきます。
削除のリンクを記載している「link_to」の中に「remote: true」を記載していることでjsファイルを探しにいってくれます。（app/views/comments/_index.html.erb に記載しています。）
「remote: true」を記載していなかった場合は、htmlファイルを探しにいきます。

# 投稿のコントローラー
```ruby:app/controllers/posts_controller.rb
  def show
    @post = Post.find(params[:id])
    @comment = Comment.new #①
    @comments = @post.comments #②
  end
```
どちらも、投稿のビュー「app/views/posts/show.html.erb」でパーシャルに渡す変数として使用します。
 #①：入力フォームで使用するインスタンスを作成しています。

 #②：コメント一覧表示で使用するためのコメントデータを入れています。

# 投稿のビュー
```html:app/views/posts/show.html.erb
  <div>
    <h4>コメント</h4>
    <div id="comments_area"><!-- #① -->
      <!-- 投稿されたコメント一覧をブログの詳細ページに表示するためのrender -->
      <%= render partial: 'comments/index', locals: { comments: @comments } %>
    </div>  
    <% if user_signed_in? %>
      <!-- コメント入力欄をブログの詳細ページに表示するためのrender -->
      <%= render partial: 'comments/form', locals: { comment: @comment, post: @post } %>
    <% end %>
  </div>
```
 #①：「id="comments_area"」がポイントです。
このidをターゲットにして、このdiv内をAjaxで書き換えます。
このdivの内側に、renderを使ってパーシャルを表示します。
@commentをパーシャル内で使うローカル変数commentとして渡しています。

[（参考：パーシャルを利用するときのrenderの使い方について）
render - リファレンス - - Railsドキュメント](
http://railsdoc.com/references/render)

[（参考：ローカル変数について）Railsの部分テンプレートからインスタンス変数を参照するのはやめよう。](
https://kadoppe.com/archives/2013/09/rails-partial-template-instance-variables.html)

どちらもとても勉強になりました。

# パーシャル部分のビュー
```html:app/views/comments/_index.html.erb
<% comments.each do |comment| %>
  <% unless comment.id.nil? %>
    <p><%= link_to "#{comment.user.name}さん", user_path(comment.user.id) %></p>
    <p>コメント：<%= comment.content %></p>
    <% if comment.user == current_user %>
      <p><%= link_to 'コメントを削除する', post_comment_path(comment.post_id, comment.id), method: :delete, remote: true %></p>
    <% end %>
  <% end %>
<% end %>
```
パーシャルはファイル名の先頭に「_」を入れます。
投稿のビューから渡したローカル変数(comments)を comment に入れて一つずつ表示しています。

ポイントは、コメントの削除のところで「(comment.post_id, comment.id)」と投稿のidとコメントのidを渡す必要があることと、5. コメントコントローラーのところでも触れましたが「remote: true」をつけることによって、コントローラーでjsファイルを探しにいってもらうことです。
idをcomment.post_idとcomment.idの2つ渡す必要があるのは、削除したいコメントを指定するには「post/12/comment/31」のようにpost_idとcomment_idを指定する必要があるためです。

```html:app/views/comments/_form.html.erb
<%= form_with(model: [post, comment] ) do |form| %>
  <div>
    <%= form.text_area :comment_content %>
  </div>
  <div class="actions">
    <%= form.submit "コメントをする" %>
  </div>
<% end %>
```
ポイントは、「model: [post, comment]」とすることです。
post, commentはそれぞれ、7. 投稿のビューで渡しているインスタンスのローカル変数です。
投稿に紐づいたコメントを生成するため、ここでpost、commentのインスタンスを渡すことが必要になります。

# jsファイル
```js:app/views/comments/index.js.erb
$("#comments_area").html("<%= j(render 'index', { comments: @comment.post.comments }) %>")
$("textarea").val('')
```
とてもシンプルです。
このファイルに、7. 投稿のビューの中で id = "comments_area"とした箇所を書き換える処理を記載しています。

「$("#comments_area")」が id = "comments_area"をターゲットとする記載です。
ターゲットとした箇所を、「render 'index'」で指定している8. パーシャル部分のビューの内容で書き換えています。

{ comments: @comment.post.comments }で、@comment.post.comments をローカル変数 comments に入れて渡しています。
@comment.post.comments は、コメント一覧表示するのに必要なコメント全件です。

「$("textarea").val('')」によって、コメント入力後のコメント入力欄を空にしています。

# 最後に
以上で、Ajaxを用いた動的なコメント投稿・削除機能が実装できたはずです！
※分かりやすくするために、デザイン面のbootstrapに関するコードは消しています。

間違っている箇所、分かりにくい箇所等あれば是非教えてください！