Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

ブログなどの投稿ページにマストなコメント投稿、削除機能を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に関するコードは消しています。

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

miyazaki_yusuke
サービス作りが大好きなRailsエンジニアです。新卒から4年間は銀行で営業をしていました。目標は起業家 兼 エンジニア。26歳です。サービス→Jobmiru(スキル特化の転職口コミサイト):https://www.jobmiru.com/ BigTweet:https://bigtweet.herokuapp.com/
http://ysk-pro.hatenablog.com/
fablic
満足度No.1 のフリマアプリ「ラクマ」を運営しています。
https://fril.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした