Ruby on Railsの備忘録

エンジニアになるために覚えたことを記録していきます。

ネストしたルーティングのフォームの書き方について

以前に課題をやって理解できていなかった親子構造であるネストしたルーティングのフォームの書き方について復習していきます。

実装概要

掲示板アプリを作成しています。

掲示板詳細ページにて掲示板に対したコメント投稿を行えるようにする。

コメント投稿フォームは掲示板詳細ページ内に実装。

コメント一覧も掲示板詳細ページに実装。

前提

ユーザー登録機能、ログイン機能、掲示板作成機能は実装済み。 UserモデルとBoardモデルは作成しており、アソシエーションを繋げています。

Commentモデルを作成

rails g model Comment body:text user:references board:references

上記をターミナルで打ち、UserモデルとBoardモデルをアソシエーションしたCommentモデルを作成していきます。

Comment.rb

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :board

  validates :body, length: { maximum: 65535 }, presence: true
end

UserモデルともBoardモデルとも1対多なのでbelongs_toで繋げていく。

掲示板詳細ページにてコメント投稿フォームとコメント一覧を記載

掲示板詳細ページを作り、そこにコメント投稿フォームとコメント一覧ページを追加していく。

コメント投稿フォーム

ここが一番難しく、理解に時間がかかるところでした。

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>掲示板詳細</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", width: 300, height: 200 %>
            </div>
            <div class='col-md-9'>
              <h3 style='display: inline;'><%= @board.title %></h3>
              <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: :short %></li>
              </ul>
            </div>
          </div>
          <p><%= @board.body %></p>
          <% if current_user.own?(@board) %>
            <div class='mr10 float-right'>
              <%= link_to edit_board_path(@board), id: 'button-edit-#{board.id}' do %>
                <%= icon 'fa', 'pen' %>
              <% end %>
              <%= link_to board_path(@board), id: 'button-delete-#{board.id}', method: :delete, data: {confirm: '削除してもいいですか?'} do %>
                <%= icon 'fas', 'trash' %>
              <% end %>
            </div>
          <% end %>
        </div>
      </article>
    </div>
  </div>

  <!-- コメントフォーム -->
  <%= render 'comments/form', {board: @board, comment: @comment} %>

  <!-- コメントエリア -->
  <% if @comments.present? %>
    <%= render @comments %>
  <% end %>
</div>

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.text_field :body, class: 'form-control mb-3', placeholder: 'コメント' %>

      <%= f.submit '投稿', class: 'btn btn-primary' %>

    <% end %>
  </div>
</div>

まず、show.html.erbでパーシャル化したフォームをレンダリングしています。 どの掲示板にコメントしているのかわからないとどの掲示板にコメントしているのかわからなくなり、困ります。

そのため、commentインスタンスにはboard_idが必要です。

なのでurlでboard_idを取得するためboardインスタンスをコメント投稿フォームのurlに組み込む必要があります。

上記のform_withは省略された形で、省略せずに書くとこのような形になります。

<%= form_with model: comment, url: board_comment_path(board, comment), local: true do |f| %>

board_comment_path/boards/:board_id/commentsというurlになるため、boardインスタンスとcommentインスタンスを渡してあげるようにしないとエラーが起こります。 これを簡略化したものがこちらのフォームの書き方となります。

<%= form_with model: comment, url: [board, comment], local: true do |f|%>
コメント一覧フォーム

コメント一覧はパーシャルである_comment.html.erbを作成し、<%= render @comments %>レンダリングしています。

controllerの実装

この部分も理解に苦労しました。

boards_controller showアクションの実装

boards_controller.rb

class BoardsController < ApplicationController
before_action :set_board, only: [:show, :edit, :update, :destroy]
  def index
    @boards = Board.all.order(created_at: :desc)
  end

  def new
    @board = Board.new
  end

  def create
    @board = current_user.boards.create(board_params)
    if @board.save
      redirect_to boards_path, success: '掲示板作成しました'
    else
      flash.now[:danger] = '掲示板作成に失敗しました'
      render :new
    end
  end

  def show
    @comments = @board.comments.all.order(created_at: :desc)
    @comment = Comment.new
  end

  def edit; end

  def update
    if @board.update(board_params)
      redirect_to board_path(@board), success: '掲示板を更新しました'
    else
      render :edit
      flash[:danger] = '掲示板を更新できませんでした'
    end
  end

  def destroy
    @board.destroy!
    redirect_to boards_path, success: '掲示板を削除しました'
  end

private

  def board_params
    params.require(:board).permit(:title, :body, :board_image, :board_image_cache)
  end

  def set_board
    @board = current_user.boards.find(params[:id])
  end
end

え?なんでboards_controllerのshowアクションに@commentsや@commentを書いてるの?

なぜかというとコメント投稿フォームやコメント一覧ページはboardsのshowページに書かれているからです。いくらcomments_controllerに@commentのインスタンス変数を定義していても、comments/show.html.erbというページに書かれているわけではないのでboards/show.html.erbのページには反映されないというわけです。

@coomentsは指定されたboardのコメントだけを取得して表示させたいので@board.comments.allで取得しています。order(created_at: :desc)は作成日時を降順に並べ替えています。

comments_controllerの実装

comments_controller.rb

class CommentsController < ApplicationController

  def create
    comment = current_user.comments.create(comment_params)

    if comment.save
      redirect_to board_path(comment.board), success: 'コメントを投稿しました'
    else
      redirect_to board_path(comment.board)
      flash.now[:danger] = 'コメントの投稿に失敗しました'
    end
  end

  def destroy
    comment = Comment.find(params[:id])
    comment.destroy
    redirect_to board_path(comment.board), success: 'コメントを削除しました'
  end

private

  def comment_params
    params.require(:comment).permit(:body).merge(board_id: params[:board_id])
  end

end

まず、知らなかったのがストロングパラメーターにmergeメソッドがあるということ。mergeメソッドを使うことで直接的にユーザーから受け取った値でなくとも値の情報を取得して追加して処理できます。

私はこのメソッドを知らずにわざわざ@comment.board_id = (params[:board_id])で取得していました。mergeメソッドの方が使いやすくて綺麗で見やすくていいですね。

current_userメソッドからログインしているuser_idを引っ張ってきてコメントを作成しています。

参考にした記事

【Ruby on Rails】ストロングパラメータとは何か? また記載方法は? - Qiita