ブログなどの投稿ページにマストなコメント投稿、削除機能をAjaxを用いて動的に作ってみました。コメント投稿、削除でいちいちページ遷移するより圧倒的に使いやすいです。
ネット上に自分の求めていたジャストの記事が無かったので、今回の実装をまとめてみました!
個人ブログに同様の内容を書いておりましたが、技術的な内容はQiitaに集約することにしたので、こちらにも投稿します。
(参考)個人ブログ
投稿ページの実装、JQueryの読み込みはできている前提です。
※ バージョンは、Ruby:2.3.0、Rails:5.1.4です。
スキーマ(コメントテーブル)
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」カラムの作成が必要です。
モデル(アソシエーション設定)
belongs_to :user
belongs_to :post
validates :content, presence: true
ユーザーはたくさんのコメントを持てる、投稿はたくさんのコメントは持てると、一対他(ユーザー・投稿:一、コメント:他)の関係になっているので、コメントのモデルは「berongs_to」でのアソシエーションになります。
コメントの内容が無いとコメントの意味が無いので、コメント内容必須のバリデーションを設定しています。
belongs_to :user
has_many :comments, dependent: :destroy
ユーザーはたくさんの投稿を持てる、投稿はたくさんのコメントを持てる、の関係なので投稿のアソシエーションはこのようになります。
「dependent: :destroy」は、この場合「投稿が削除された時に、同時にコメントも消去する」という意味です。
コメントだけ残ってしまっても意味が無いので記載が必要です。
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
同様の考え方です。
ルーティング
resources :posts do
resources :comments
end
コメントがどの投稿へのものであるかを識別するために、ルーティングのURLに投稿のIDを含める必要があります。
具体的には「/post/12/comment/」といったURLになります。(ネストする、と言います。)
12がpost_idです。
(参考)Rails のルーティング | Rails ガイド
コメントコントローラー
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ファイルを探しにいきます。
投稿のコントローラー
def show
@post = Post.find(params[:id])
@comment = Comment.new #①
@comments = @post.comments #②
end
どちらも、投稿のビュー「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の部分テンプレートからインスタンス変数を参照するのはやめよう。
どちらもとても勉強になりました。
パーシャル部分のビュー
<% 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を指定する必要があるためです。
<%= 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ファイル
$("#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に関するコードは消しています。
間違っている箇所、分かりにくい箇所等あれば是非教えてください!