LoginSignup
0
0

【Ruby on Rails備忘録】 コメント機能 返信機能つき

Posted at

初めに

レシピ投稿サイトを作成中の初学者です。
自分の記録のために書いてますが、どなたかの参考にもなれたのなら幸いです。

前提

コメントモデルは作成されているものとして進めていきます。
コメントモデルのカラムは以下のように設定しました。

  create_table "comments", force: :cascade do |t|
    t.text "body"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "recipe_id"
    t.integer "user_id"
    t.integer "parent_id"
    t.integer "reply_to_id"
  end

後にこのカラムの意味は解説します。

モデルにアソシエーションの追加

userモデルとrecipeモデルに関連づけます

app/models/recipe.rb
class Recipe < ApplicationRecord
  has_many :comments, dependent: :destroy
end
app/models/user.rb
class User < ApplicationRecord
  has_many :comments, dependent: :destroy
end
app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :recipe
  belongs_to :user
  belongs_to :parent, class_name: 'Comment', optional: true
  has_many :replies, class_name: 'Comment', foreign_key: :parent_id, dependent: :destroy
  belongs_to :myreply, class_name: 'Comment', foreign_key: :reply_to_id, optional: true
end

Commentモデルは返信機能にちなんだモデル設計となっています。
説明します。

belongs_to :parent, class_name: 'Comment', optional: true

これは返信先のコメントのみを格納するための関連名です。
youtubeで言うところのこの部分ですね。

c5abe0683f35c32a968c59313728ab72.png

次行きます。

has_many :replies, class_name: 'Comment', foreign_key: :parent_id, dependent: 

これは返信コメントを返信先ごとにまとめたものですね。
youtubeで言うところのこのコメントたちです。

904e87a8a256c1da8b5ce7ba8e5f7df8.png

foreign_key: :parent_id

を目印に仕分けをしています。

次行きます。

belongs_to :myreply, class_name: 'Comment', foreign_key: :reply_to_id, optional: true

これは返信コメントに対して返信するコメントを格納しています。
youtubeで言うところのこの赤枠部分です。

61b55c2dccb40652e94dd3e3dfcccad3.png

少し自分で書いていても混乱してきました笑
まあこれらの関連名に格納していくことでコメントが誰に返信しているかわかるようになっています。

ルーティングの設定

config/routes.rb
Rails.application.routes.draw do
  resources :recipes do
    resources :comments, only: [:create, :destroy, :update]
  end
end

コメントのルーティングはレシピにネストされた形で描きます。
これでレシピ一つ、一つに対して、コメントが個々に割り振られるようになりました。

Commentsコントローラの作成

class CommentsController < ApplicationController
  before_action :set_recipe

  def create
    comment = @recipe.comments.build(comment_params)
    if comment.save
    end
  end

  private
  
  def comment_params
    params.require(:comment).permit(:body, :parent_id, :reply_to_id)
  end

  def set_recipe
    @recipe = Recipe.find(params[:recipe_id])
  end
end

今回はコメントの作成部分に焦点を当てて解説します。
まずset_recipeです。これはURLから受け取ったidを使ってレシピを検索し,@recipeに設定しています。
before_actionで全てのアクションが実行される前に設定させています。

次にコメントの作成部分である、createアクションです。

@comment = @recipe.comments.build(comment_params)

このコードでコメントのインスタンスを作っています。
インスタンスはかんたんにいうと変数みたいなものです。
この変数(インスタンス)にコメントの中身が入れられ、

if comment.save

で保存されます。

viewで使う変数

次はviewに渡す変数を決めます。
以下のように設定してください。

app/controllers/recipes_controller.rb
def show
  @recipe = Recipe.includes(:steps).find(params[:id])
  @comments = @recipe.comments.includes(:user, replies: [:user, {myreply: :user}]).order(created_at: :desc)
  @comment = @recipe.comments.new
  @replies = Comment.includes(:user).where.not(reply_to_id: nil)
  @parent_comments = @comments.where(parent_id:nil)
end

順に解説していきます。

@recipe = Recipe.includes(:steps).find(params[:id])

これはレシピ詳細画面で見ているレシピを取得しています。URLに書かれたidのレシピですね。
N+1問題が起きないようにincludes(:steps)でレシピの手順のモデルごとクエリを発行しています。まあ今回関係ないので省略ですね。

@comments = @recipe.comments.includes(:user, replies: [:user, {myreply: :user}]).order(created_at: :desc)

これはレシピに付属するコメントを全て取得しています。
includes(:user, replies: [:user, {myreply: :user}])は先ほど同様N+1問題避けです。
order(created_at: :desc)はコメントを新しい順にするためのものです。

@comment = @recipe.comments.new

これはコメントを作成する際に新しくインスタンス(変数)を作らないといけないので書きました。
view側でも設定できますが、自分はshowアクションに書いておきました。

@replies = Comment.includes(:user).where.not(reply_to_id: nil)

これは返信先が親コメントだけへの返信コメントをクエリで取得した変数です。
これですね。

904e87a8a256c1da8b5ce7ba8e5f7df8.png

view

続いてコメントをレンダリングするhtmlの中身についてです。
このように書きます。

app/views/recipes/show.html.slim
  - @parent_comments.each do |comment|
    = render partial: 'shared/comment/comment_body', locals: {recipe: @recipe, comment: comment }
      
    - comment.replies.each do | reply |
      = render partial: 'shared/comment/comment_reply', locals: { recipe: @recipe, comment: comment, replycomment: reply }

解説します。

- @parent_comments.each do |comment|

これは先ほど格納した親コメントを一つずつ取り出すコードです。
そして

= render partial: 'shared/comment/comment_body', locals: {recipe: @recipe, comment: comment }

に取り出したcommentを渡すことで親コメントを表示させることができます。

そしてその親コメントを取り出した状態でまたeach文を回します。

- comment.replies.each do | reply |

これはその親コメントに存在する返信コメント達でeach文を回しています。
そして取り出したその返信コメントを

= render partial: 'shared/comment/comment_reply', locals: { recipe: @recipe, comment: comment, replycomment: reply }

に渡して返信コメント画面に表示させます。
これは返信コメントであれば全て表示されます。
返信先への返信コメントもまとめて表示です。

親コメントのviewはこのようになってます。

.container.w-95.pt-3
  p #{comment.user.name} さん #{comment.created_at.strftime('%Y/%m/%d %H時%M分')}

親コメントは返信コメントに比べてカードが大きめです。
視覚的にわかりやすくしました。

返信コメントは

.container.w-75.pt-3
  -if replycomment.myreply.present?
    p 返信先: #{link_to replycomment.myreply.user.name,user_path(replycomment.myreply.user)}
  = replycomment.body

返信コメントはカード小さめです。
また

p 返信先: #{link_to replycomment.myreply.user.name,user_path(replycomment.myreply.user)}

は返信コメントの中でも返信先を持つものだけ表示される返信先への名前ですね。
これで返信への返信を表現しています。

formのview

コメント入力フォームはこのようになってます。

= form_with model: [recipe, Comment.new], url: recipe_comments_path(recipe), local: true, html: { class: "unique-form-class" } do |f| 
  .row_margin
    = f.text_field :body, placeholder: 'コメントを入力してください', class: 'form-control' 
  = f.submit 'コメントを作成する'

親コメントが持つ返信フォームはこのようになっています。

= form_with model: [@recipe, Comment.new], method: :post, id: "replyFormTemplate", local: true do |f|
  .row_margin
    p 返信先 #{comment.user.name}
    = f.text_field :body, placeholder: '返信を入力してください', class: 'form-control' 
    = f.hidden_field :parent_id, value: comment.id, id: "parent_id"
    = f.hidden_field :id
  .text-end
    = f.submit '返信する', class: 'btn btn-warning text-white'

返信コメントが持つ返信フォームはこのようになっています。

= form_with model: [recipe, @comment], id: "replyFormTemplate", local: true, html: { class: "unique-form-class" } do |f|
  .row_margin id = "aaa"
    /# 返信コメント主名前を表示
    p 返信先 #{replycomment.user.name}
    = f.text_field :body, placeholder: '返信を入力してください', class: 'form-control' 
    = f.hidden_field :parent_id, value: comment.id, id: "parent_id"
    = f.hidden_field :reply_to_id, value: replycomment.id, id: "reply_to_id"
  .text-end
    = f.submit '返信する', class: 'btn btn-warning text-white'

違いは以下のようになってます。

親コメント
parent_id なし
reply_to_id なし

親コメントへの返信
parent_id あり
reply_to_id なし

返信コメントへの返信
parent_id あり
reply_to_id あり

これらの要素でコメントの状態を表しています。

以上で完成です。

完成系はこのような形です。

Image from Gyazo

参考記事
https://qiita.com/asami___t/items/b47d90e2f565f5836039

0
0
0

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
0
0