Ryota400’s blog

エンジニアを目指して書いてます。

コメント機能

モデルの作成

userとboardに紐ずいたcommentモデルを作成します

bundle exec rails generate model comment body:text user:references board:references

db/migrate/YYYYMMDDhhmmss_create_comments.rb

class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.text :body, null: false
      t.references :user, foreign_key: true
      t.references :board, foreign_key: true

      t.timestamps
    end
  end
end

モデルを作成する際にreferencesとすることでindexと外部キー制約(foregin_key: true)が自動で追加される。indexをuser_idとmicropost_idに追加することによってそれぞれに関連したコメントを探す際にデータを高速に調べられるようになり、外部キー制約がつくことによってuser_id(micropost_id)にはUserテーブルに存在するidのみデータベースレベルで保存するようになる。 また、コメントが空だとコメント機能の意味をなさないためnull:falseを追加する。 commentモデルに、bodyのバリデーションを追加

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :board

  validates :body, presence: true, length: { maximum: 65_535 }
end

自動でbelongs_toで一対一の関連付けができている。User,Boardモデルにはそれぞれ手動で追加する必要がある。また、バリテーションも追加する。テーブル作成の時点でcontentにはnull:falseで空で保存させないようにしていますが空文字("")は保存できます。なのでpresence:trueを追加することによって空文字も拒否するようになる。また、バリデーションに基づいたエラーメッセージも保存されます。

Boardモデルにコメントとの関連を追加

掲示板とコメントが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

ユーザーとコメントが1対多の関連であることをモデルに追加する。

class User < ApplicationRecord
  authenticates_with_sorcery!

  has_many :boards, dependent: :destroy
  has_many :comments, dependent: :destroy

Comenntsコントローラーの作成、ルーディングの設定

rails g controller comments
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

ルーティングの設定

 resources :boards, only: %i[index new create show] do
    resources :comments, only: %i[create], shallow: true
  end
end

commetsはboardsとネストして親子の関係を持たせる。こうすることによってコメントを作成する際に関連しているmicropostのidを取得することが容易になる。 shallowオプションとは何か? ルーティングの記述を複雑にせず、かつ深いネストを作らないという絶妙なバランスを保つことのできるオプション

対応するビューの作成

app/controllers/boards_controller.rb

def show
    @board = Board.find(params[:id])
    @comment = Comment.new
    @comments = @board.comments.includes(:user).order(created_at: :desc)
  end

コメントのビューを実装する 掲示板の編集と削除のボタンは、掲示板の一覧と詳細ページで同じものを表示するので、部分テンプレートとして作成する。 app/views/boards/_crud_menus.html.erb

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

app/views/boards/_board.html.erb

<%= image_tag board.board_image_url, class: 'card-img-top', size: '300x200' %>
      <div class="card-body">
        <h4 class="card-title">
          <%= link_to board.title, board_path(board) %>
        </h4>
        <%= render 'crud_menus', board: board %>
        <ul class="list-inline">
          <li class="list-inline-item">
            <%= icon 'far', 'user' %>

app/views/boards/show.html.erb

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

app/models/user.rb

def own?(object)
    id == object.user_id
  end

ユーザーのコメントであるかを判定するメソッドをuserモデルに追加する。

コメントフォームのテンプレートを作成する。 app/views/comments/_form.html.erb

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

コメント一覧のテンプレートを作成する。 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>

app/views/comments/_comment.html.erb

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

参考文献

qiita.com

qiita.com