Rails 掲示板詳細画面の追加/コメント機能の実装 手順 (自分用)
#コメントモデルを作成する
$ bundle exec rails generate model comment body:text user:references board:references
・UserモデルとBordモデルを参照するようにCommentモデルを作る。
###マイグレーションファイル
class CreateComments < ActiveRecord::Migration[5.2]
def change
create_table :comments do |t|
t.text :body, null: false #空にならないようにnull制約をfalseにする
t.references :user, foreign_key: true
t.references :board, foreign_key: true
t.timestamps
end
end
end
・モデルを作る際に「user:references」「bord:references」を設定したことによってそれぞれの「foreign_key(外部キー)」を持つようになる。
###commentモデルに、bodyのバリデーションを追加
class Comment < ApplicationRecord
belongs_to :user
belongs_to :board
validates :body, presence: true, length: { maximum: 65_535 }
end
・65字以上535字以内のバリデーションを追加する。
$ bundle exec rails db:migrate
・マイグレートする
#BoardモデルとUserモデルにコメントとの関連を追加する
・掲示板とコメントが1対多の関係であることをモデルに追加
class Board < ApplicationRecord
mount_uploader :board_image, BoardImageUploader
belongs_to :user
has_many :comments, dependent: :destroy
validates :title, presence: true, length: { maximum: 255 }
validates :body, presence: true, length: { maximum: 65_535 }
end
・has_manyはテーブル同士を関連づけるもの。そのクラス(ここではBord)のidを外部キーとして抱える他のクラス(ここではComment)があり、BordはCommentを複数登録可能であるということ。*has_many(Bord)はbelongs_to(Comment)を何個も投稿できるよ!みたいなイメージ。
・「dependent: :destroy」オプションをつけることによって「commentに紐づいたbordが消されたら該当commentも削除」という作業を行なってくれる。親にあたるbordが削除された際に、子にあたるcommentだけが残ると「comment(子)があるのにbord(親)がいない」ということになり、整合性が取れずバグの原因になる為、「dependent: :destroy」を使う。
・ユーザーとコメントが1対多の関連であることをモデルに追加
class User < ApplicationRecord
authenticates_with_sorcery!
has_many :boards, dependent: :destroy
has_many :comments, dependent: :destroy
・Bordモデルと同様
#コメントのコントローラーを追加する
$ bundle exec rails generate controller comments
・コメントのコントローラーを生成
###routes.rbに、コメントのrouteを追加
resources :boards, only: %i[index new create show] do
resources :comments, only: %i[create], shallow: true
end
end
・ルーティングのネスト
・ルーティングをネストすることで親のidを子のパスに含めることができる。
・idをパスに含めることでアソシエーション先のレコードのid(ここではbord_id)をparamsに追加してコントローラーに送り、コントローラーで取得することができるようになる。
・shallowは、コレクション(index/new/createのような、idを持たないアクション)だけを親(board)のスコープの下で生成するもの。つまり、createアクションだけはcommentsテーブルのレコードを生成する際にboardのid(board_id)が欲しいのでここだけ深いネストをして、それ以外は浅いネスト(shallow)にする。
*メンバー(show/edit/update/destroyのようなidを必要とするアクション)をネストに含めない。つまり、メンバーはすでにboard_idを持っていて、必要なのは、commentsテーブルのidなので浅いネストにするということ。
###コントローラーを設定
class CommentsController < ApplicationController
def create
comment = current_user.comments.build(comment_params)
if comment.save
redirect_to board_path(comment.board), success: t('defaults.message.created', item: Comment.model_name.human)
else
redirect_to board_path(comment.board), danger: t('defaults.message.not_created', item: Comment.model_name.human)
end
end
private
def comment_params
params.require(:comment).permit(:body).merge(board_id: params[:board_id])
end
end
・「current_user.comments.build(params)」でログインしているユーザーのidをコメントの「user_id」に代入して、「(comment_params)」を引数に「build(新規作成)」が行われている。
・「comment_params」ではストロングパラメーターを活用しており、params[:comment]からCommentモデルのカラムである「body」と「bord_id」にパスに含まれるidをparams[:board_id]から取得して、mergeしている。(ルーティングでネストしたことで、「boards/:board_id/comments」となっている)
#掲示板詳細ページのコントローラを追加する
・コメントの新規作成は投稿の詳細でおこなうので、掲示板詳細画面(show)でコメントの新規作成フォーム(@comment使う)とコメント一覧(@comments使う)を表示
def show
@board = Board.find(params[:id]) #bordsテーブルのidを取得している
@comment = Comment.new #コメントの新規作成
@comments = @board.comments.includes(:user).order(created_at: :desc)
end
#掲示板の編集と削除のボタンの部分テンプレート
・掲示板の編集と削除のボタンは、掲示板の一覧と詳細ページで同じものを表示するので、部分テンプレートとして作成しておく。
<ul class='crud-menu-btn list-inline float-right'>
<li class="list-inline-item">
<%= link_to '#', id: "button-edit-#{board.id}" do %>
<%= icon 'fa', 'pen' %>
<% end %>
</li>
<li class="list-inline-item">
<%= link_to '#', id: "button-delete-#{board.id}" do %>
<%= icon 'fas', 'trash' %>
<% end %>
</li>
</ul>
#掲示板詳細画面
<% content_for(:title, @board.title) %> #①
<div class="container pt-5">
<div class="row mb-3">
<div class="col-lg-8 offset-lg-2">
<h1><%= t('.title') %></h1>
<!-- 掲示板内容 -->
<article class="card">
<div class="card-body">
<div class='row'>
<div class='col-md-3'>
<%= image_tag @board.board_image.url, class: 'card-img-top img-fluid', size: '300x200' %>
</div>
<div class='col-md-9'>
<h3 class="d-inline"><%= @board.title %></h3>
<%= render 'crud_menus', board: @board %> #②
<ul class="list-inline">
<li class="list-inline-item">by <%= @board.user.decorate.full_name %></li>
<li class="list-inline-item"><%= l @board.created_at, format: :long %></li>
</ul>
</div>
</div>
<p><%= simple_format(@board.body) %></p> #③
</div>
</article>
</div>
</div>
<!-- コメントフォーム -->
<%= render 'comments/form', { board: @board, comment: @comment } %>
<!-- コメントエリア -->
<%= render 'comments/comments', { comments: @comments } %>
</div>
*①<% content_for(:title, @board.title) %> #タイトルを指定している。「@board.title」はja.ymlファイルで指定している文字を引数に「:titleに代入している」
*②<%= render 'crud_menus', board: @board %> #掲示板の編集と削除ボタンをパーシャルで呼び出している
*③simple_formatはRailsのヘルパーメソッドで、テキストを改行などつけてトランスフォームしてくれる。
*④コメントを直接書き込むフォーム。「render 'comments/form'」は、「app/views/comments/_form.html.erb」を呼び出している(後で作る)。{ board: @board, comment: @comment } は、ローカルオプションが省略されている形。パーシャルに、@board と @commentを引数として渡している。呼び出し元でコントローラーと関連付けることで、パーシャルを使い回しやすくなる。
*⑤はコメント一覧が表示されるようになっている。詳しくは下記で説明
#コメント一覧画面
<%= render 'comments/comments', { comments: @comments } %>は「app/views/comments/_comments.html.erb」を呼び出している
<div class="row">
<div class="col-lg-8 offset-lg-2">
<table id="js-table-comment" class="table">
<%= render comments %>
</table>
</div>
</div>
*<%= render comments %>部分は省略されているので省略せずに書くと、
「<%= render partial: "comment", collection: @comments %>」となる。
これはパーシャルの「app/views/comments/_comment.html.erb」を呼び出し、「boardコントローラ」の「showアクション」にある「@comments」の数だけパーシャル(app/views/comments/_comment.html.erb)を呼び出す。ということになっている。
*<%= render comments %>となっているのは、ファイル名(_comment.html.erb)とインスタンス変数名@comment(複数形のsを除いた)が一致しており、かつeach文のように繰り返し表示を行いたい場合は、上記のような形に省略できるようになっているから。
#コメントフォームのテンプレートを作成
<div class="row mb-3">
<div class="col-lg-8 offset-lg-2">
<%= form_with model: comment, url: [board, comment], local: true do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :body %>
<%= f.text_area :body, class: 'form-control mb-3', id: 'js-new-comment-body', row: 4, placeholder: Comment.human_attribute_name(:body) %>
<%= f.submit t('defaults.post'), class: 'btn btn-primary' %>
<% end %>
</div>
</div>
・<%= form_with model: comment, url: [board, comment], local: true do |f| %>
・「form_with model: comment」でCommentモデルを基にフォームが作られる。@commentにしていないのは、パーシャルは「new」「edit」や、今回のように「show」で呼び出し、呼び出し元でコントローラーのインスタンスを代入して使い回したいのでインスタンス変数にしてコントローラーと結びつけてしまうと、使い勝手が悪くなる為。
・「url: [board, comment]」 :urlオプションでデータの送信先を指定している。データの送信先を指定しているのはデフォルトだと、createかeditに送信されてしまうのを防ぐ為。今回は、boardコントローラーのshowアクションでコメントの新規作成を行う際の「@comment = Comment.new」インスタンスのレコードと、コメントをする際の親に当たる投稿を「@board = Board.find(params[:id])」インスタンスのレコードを呼び出している。「@board」でコメントする投稿を取得して「@comment」でコメントの新規作成を行なっている。呼び出し元(showアクション)で「url: [board, comment]」に@boardと@commentを代入している。また、ルーティングを親子関係でネストしているのでurl(データの送信先)に[board, comment]を指定しなければならない。*ここでもインスタンス変数にしてないのはコントローラーと結びつけると使い回しにくくなる為。
#コメント作成者にだけ、編集と削除ボタンを表示
・コメントの編集・削除ボタン表示の判定する時の条件分岐は、Userモデルにインスタンスメソッドとして記載する。
*条件分岐などををControllerに記載すると後で分かりづらくなるのでControllerやViewでなく、Modelに記載して呼び出すことで、メンテナンス時にModelだけの変更で済むようになるから。
*自分の作成したリソースの判定はUserモデルにまとめて記載する」方針だと各リソースに対しての判定ロジックをUserモデルを見るだけで済むようになる。
def own?(object)
self.id == object.user_id #selfは省略できる。
end
・Userモデルにインスタンスメソッドを定義する。
*「self.id == object.user_id」を読み解くと、「self」はコントローラー側で定義されたインスタンスとして使える。ここ(app/views/comments/_comment.html.erb」)では、「current_user」というヘルパーメソッドに対してインスタンスメソッド「own?(object)」を呼び出している。この場合インスタンスメソッド内では「self」が使えるようになり、「self=current_user」となっている。(「self=呼び出し元のインスタンス」で使うことも多い)
*今回のインスタンスメソッドは、「self(current_user)のidとobject(comment)のuser_idが一致しているか」という意味になる。これをif文でコメントとユーザーの一致を検証することで、コメントを作成したユーザーにしか編集と削除ボタンを表示させないようにすることができる。
<tr id="comment-<%= comment.id %>">
<td style="width: 60px">
<%= image_tag 'sample.jpg', class: 'rounded-circle', size: '50x50' %>
</td>
<td>
<h3 class="small"><%= comment.user.decorate.full_name %></h3>
<div id="js-comment-<%= comment.id %>">
<%= simple_format(comment.body) %>
</div>
<div id="js-textarea-comment-box-<%= comment.id %>" style="display: none;">
<textarea id="js-textarea-comment-<%= comment.id %>" class="form-control mb-1"><%= comment.body %></textarea>
<button class="btn btn-light js-button-edit-comment-cancel" data-comment-id="<%= comment.id %>">キャンセル</button>
<button class="btn btn-success js-button-comment-update" data-comment-id="<%= comment.id %>">更新</button>
</div>
</td>
<% if current_user.own?(comment) %>
<td class="action">
<ul class="list-inline justify-content-center" style="float: right;">
<li class="list-inline-item">
<a href="#" class='js-edit-comment-button' data-comment-id="<%= comment.id %>">
<%= icon 'fa', 'pen' %>
</a>
</li>
<li class="list-inline-item">
<a href="#" class='js-delete-comment-button' data-comment-id="<%= comment.id %>">
<%= icon 'fas', 'trash' %>
</a>
</li>
</ul>
</td>
<% end %>
</tr>