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

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

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

個人ブログに同様の内容を書いておりましたが、技術的な内容はQiitaに集約することにしたので、こちらにも投稿します。
(参考)個人ブログ

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

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

スキーマ(コメントテーブル)

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」カラムの作成が必要です。

モデル(アソシエーション設定)

app/models/comment.rb
  belongs_to :user
  belongs_to :post
  validates :content, presence: true

ユーザーはたくさんのコメントを持てる、投稿はたくさんのコメントは持てると、一対他(ユーザー・投稿:一、コメント:他)の関係になっているので、コメントのモデルは「berongs_to」でのアソシエーションになります。

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

app/models/post.rb
  belongs_to :user
  has_many :comments, dependent: :destroy

ユーザーはたくさんの投稿を持てる、投稿はたくさんのコメントを持てる、の関係なので投稿のアソシエーションはこのようになります。

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

app/models/user.rb
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy

同様の考え方です。

ルーティング

config/routes.rb
  resources :posts do
    resources :comments
  end

コメントがどの投稿へのものであるかを識別するために、ルーティングのURLに投稿のIDを含める必要があります。
具体的には「/post/12/comment/」といったURLになります。(ネストする、と言います。)
12がpost_idです。
(参考)Rails のルーティング | Rails ガイド

コメントコントローラー

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ドキュメント

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

#④:保存がされると、render :indexによって「app/views/comments/index.js.erb」を探しにいきます。
「form_with」でフォームを送信した時は、デフォルトでjsファイルを探しにいく設定になっています。
htmlファイルを探しにいってほしい場合には、form_withの後に「local: true」と記載する必要があります。
(参考:form_withについて)rails-ujs と form_with の使い方 - ボクココ
(参考:renderについて)レイアウトとレンダリング | Rails ガイド

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

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

投稿のコントローラー

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」でパーシャルに渡す変数として使用します。
#①:入力フォームで使用するインスタンスを作成しています。

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

投稿のビュー

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ドキュメント

(参考:ローカル変数について)Railsの部分テンプレートからインスタンス変数を参照するのはやめよう。

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

パーシャル部分のビュー

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を指定する必要があるためです。

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ファイル

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に関するコードは消しています。

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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.