LoginSignup
15

More than 3 years have passed since last update.

Railsでのコメント返信機能の実装

Posted at

Railsでのコメント返信機能の実装

Railsでコメントの返信機能を実装した際に実施した手順をまとめます。

目次


動作環境

OS : macOS Mojave 10.14.6
ruby : 2.6.3p62
rails : 5.2.4

やりたいこと

コメントに対して返信できる機能を実装したい。

完成イメージ
image.png

実装前のモデル

Commentモデルは1対多でUserモデルとPostモデルに紐付いている状態

schema.rb

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
comment.rb
class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :post

手順

STEP 1. マイグレーションファイルの作成

まずは自分自身のidを外部キーとするカラム(parent)を追加します。

migration
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"]が作成されます。

schema.rb
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は返信されたコメント

comment.rb

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メソッドで良いです。

CommentsController.rb

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メソッドでコメントのインスタンス変数を設定します。

PostsController.rb

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モデルでネストします。

routing.rb
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を渡していません

Show.rb
<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 %>

結果

image.png

おわりに

コメント返信機能で自分自身のidを持つモデルを構築した。
1. 現状の問題点として全ての返信は全て最初のコメントを親として持つため、最新の返信コメントを親とするように修正が必要だと感じました。
2. 返信ボタンを押すと返信フォームが表示されるように設定したい

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15