#Railsでのコメント返信機能の実装
Railsでコメントの返信機能を実装した際に実施した手順をまとめます。
目次
動作環境
OS : macOS Mojave 10.14.6
ruby : 2.6.3p62
rails : 5.2.4
やりたいこと
コメントに対して返信できる機能を実装したい。
実装前のモデル
Commentモデルは1対多でUserモデルとPostモデルに紐付いている状態
create_table "comments", force: :cascade do |t|
t.string "content", limit: 1000, default: "", null: false
t.bigint "user_id"
t.bigint "post_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
class Comment < ApplicationRecord
belongs_to :user
belongs_to :post
手順
STEP 1. マイグレーションファイルの作成
まずは自分自身のidを外部キーとするカラム(parent)を追加します。
class AddParentToComments < ActiveRecord::Migration[5.2]
def change
add_reference :comments, :parent, foreign_key: { to_table: :comments }
end
end
マイグレーションの実行!
rails db:migrate
マイグレーション後にparent_idとインデックスの張られたt.index["parent_id"]が作成されます。
create_table "comments", force: :cascade do |t|
t.string "content", limit: 1000, default: "", null: false
t.bigint "user_id"
t.bigint "post_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "parent_id"
t.index ["parent_id"], name: "index_comments_on_parent_id"
t.index ["post_id"], name: "index_comments_on_post_id"
t.index ["user_id"], name: "index_comments_on_user_id"
end
STEP 2. Commentモデルの編集
次に以下のようにbelongs_toとhas_manyで自分自身に対する 1 対 多 のアソシエーションを設定します。
ここでポイントとしてparentカラムに対してoptional:trueとすることです。
nilを許可することで返信でない通常のコメントも登録できるようにします。
:parentは返信対象となるコメントのID
:repliesは返信されたコメント
class Comment < ApplicationRecord
belongs_to :user
belongs_to :post
+ belongs_to :parent, class_name: "Comment", optional: true
+ has_many :replies, class_name: "Comment", foreign_key: :parent_id, dependent: :destroy
validates :content, presence: true, length: { in: 1..1000 }
end
STEP 3. Commentsコントローラ
CommentsコントローラのStrongパラメータに:parent_id
を追加します。
またformat.jsは今回はajaxを利用するため書いてますが
通常のredirect_toメソッドで良いです。
class CommentsController < ApplicationController
def create
@comment = current_user.comments.build(comment_params)
respond_to do |format|
if @comment.save
format.js { render :index }
else
format.html { redirect_to post_path(@comment.post), notice: '投稿できませんでした...' }
end
end
end
private
def comment_params
params.require(:comment).permit(:content, :post_id, :user_id, :parent_id)
end
end
STEP 4. View/Routing
コメントはPostモデルのshow.htmlに表示されるため、
PostsControllerのshowメソッドでコメントのインスタンス変数を設定します。
class PostsController < ApplicationController
before_action :set_post, only: %i[show edit update destroy]
before_action :authenticate_user!, only: %i[new edit update destroy]
def index
@posts = Post.all
end
def show
@comments = @post.comments
@comment = @post.comments.build #投稿全体へのコメント投稿用の変数
@comment_reply = @post.comments.build #コメントに対する返信用の変数
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :detail, :post_image, :post_image_cache, :deadline, :status, category_ids: [])
end
end
CommentはPostモデルでネストします。
Rails.application.routes.draw do
root 'posts#index'
devise_for :users
resources :posts do
resources :comments
end
end
Point 1. 2つのform_withタグは
1.コメントへの返信用
2.投稿へのコメント投稿用
Point 2. 1つ目のform_withでは返信対象のcommentを特定するためform.hidden_field :parent_id, value: comment.id
を記載していますが
2つ目のform_withには返信対象のコメントはないためparent_idを渡していません
<h4>Post Detail</h4>
<label>タスク</label>
<input type="text" readonly class="form-control show-form" value="<%=@post.title%>">
<div id="comments_area">
<h2>comments</h2>
<% @comments.each do |comment| %>
<% if comment.parent_id.present? || comment.id.blank? %>
<% next %>
<% end %>
<hr>
<p> <%= comment.user.name %> : <%=comment.content%></p>
<p> date : <%=comment.created_at.strftime("%Y-%m-%d %H:%M") %>
<div id="reply_area">
<% comment.replies.order(:created_at).each do |reply| %>
<p> <%= comment.user.name %> : <%=reply.content%></p>
<% end %>
</div>
<%= form_with(model:[@post,@comment_reply]) do |form| %>
<div class="row">
<div class="form-group col-md-6">
<p><label>Reply</label></p>
<textarea class="form-control input-form" name="comment[content]" rows="2"><%=@comment_reply.content%></textarea>
</div>
</div>
<%= form.hidden_field :post_id, value: @post.id %>
<%= form.hidden_field :parent_id, value: comment.id%>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
</div>
<% end %>
<%= form_with(model:[@post,@comment]) do |form| %>
<p><label>New Thread</label></p>
<textarea class="form-control input-form" name="comment[content]" rows="5"><%=@comment.content%></textarea>
<%= form.hidden_field :post_id, value: @post.id %>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
</div>
<%= link_to '編集', edit_post_path(@post) %>
<%= link_to '戻る', posts_path %>
結果
おわりに
コメント返信機能で自分自身のidを持つモデルを構築した。
- 現状の問題点として全ての返信は全て最初のコメントを親として持つため、最新の返信コメントを親とするように修正が必要だと感じました。
- 返信ボタンを押すと返信フォームが表示されるように設定したい