Edited at

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

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