Billy Blob Snortin
Billy Blob Snortin

Reputation: 1201

Unable to render a nested resource's errors on parent resource show page

I have a basic "blog" application with posts and comments. I'd like to allow users to comment on a post from post#show and I'm not exactly sure how to present comment validation errors on the post.

The code below nests comments under posts and will successfully create comments. The issue when when a validation fails (such as a blank comment) the visitor is redirected to the post show page but the comment error messages are lost.

Right now CommentsController#create is redirecting the user to the post he was viewing. Alternatively I had tried render 'posts/show' instead of a redirect but it ends up rendering the page at posts/1/comments instead of posts/1.

Any help getting errors for comment validations displayed on the posts/show template would be greatly appreciated.

Routes

# config/routes.rb
Rails.application.routes.draw do
  resources :posts do
    resources :comments, only: [:create]
  end
end

Models

class Comment < ApplicationRecord
  belongs_to :post  
  validates :content, presence: true, length: { in: 6..20 }
end

class Post < ApplicationRecord
  has_many :comments
end

Controllers

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  end
end


# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    @post = Post.find(params[:post_id])
    @comment = @post.comments.build(comment_params)

    if @comment.save
      # SAVE works fine
      redirect_to @post, notice: 'Comment was successfully created.'
    else
      # ERROR displays nothing on the post show page
      redirect_to @post
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:content, :post_id)
  end
end

Views

# app/views/posts/show.html.erb
<p id="notice"><%= notice %></p>

<p>
  <strong>Title:</strong>
  <%= @post.title %>
</p>

<h2>Comments</h2>

<table>
  <thead>
    <tr>
      <th>Content</th>
    </tr>
  </thead>

  <tbody>
    <% @post.comments.each do |comment| %>
      <tr>
        <td><%= comment.content %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<h3>New Comment</h3>

<%= render 'comments/form', post: @post, comment: @post.comments.build %>
# app/views/comments/_form.html.erb
<%= form_with(model: [post, comment], local: true) do |form| %>
  <% if comment.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(comment.errors.count, "error") %> prohibited this comment from being saved:</h2>

      <ul>
      <% comment.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :content %>
    <%= form.text_field :content %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

Note: I am trying to avoid accepts_nested_attributes_for if possible as for more complex use cases I find it to be extremely confusing.

Upvotes: 0

Views: 855

Answers (1)

Bartosz Pietraszko
Bartosz Pietraszko

Reputation: 1407

You don't see the error message because on a failed save attempt, you redirect to a different controller/action; unsaved @comment object is not only gone, but also overwritten in view layer when comment form is rendered. Try rendering the posts/show view in CommentsController#create action.

if @comment.save
  # SAVE works fine
  redirect_to @post, notice: 'Comment was successfully created.'
else
  render 'posts/show'
end

For this to work, you also need to move instantianting of a new comment from view to PostsController#show action.

# app/controllers/comments_controller.rb
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    @comments = @post.comments
    @comment = Comment.new(post: @post)
  end
end

Iterate on @comments loaded in controller. New commment shouldn't be included among them. When rendering the form, use @comment variable. Remember to associate new comment with post before saving it.

<h2>Comments</h2>

<table>
  <thead>
    <tr>
      <th>Content</th>
    </tr>
  </thead>

  <tbody>
    <% @comments.each do |comment| %>
      <tr>
        <td><%= comment.content %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>
<h3>New Comment</h3>

<%= render 'comments/form', post: @post, comment: @comment %>

This way posts/show view should work with both controller actions; state of @comment object, along with it's errors, should be preserved after save attempt.

Upvotes: 2

Related Questions