Showing associated model errors in Rails 6 form

Viewed 845

I'm using Rails 6.0.3.4 and Ruby 2.7.2. Using the Rails getting started tutorial as an example, I'm wondering how to show form validation errors of an associated model.

Show page

<p>
  <strong>Title:</strong>
  <%= @article.title %>
</p>
 
<p>
  <strong>Text:</strong>
  <%= @article.text %>
</p>
 
<h2>Comments</h2>
<%= render @article.comments %>
 
<h2>Add a comment:</h2>
<%= render 'comments/form' %>
 
<%= link_to 'Edit', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>

Comments form (this is the form in question)

<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

Models

class Comment < ApplicationRecord
  belongs_to :article

  validates :commenter, presence: true
end
class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
  validates :title, presence: true,
                    length: { minimum: 5 }
end

For a single model articles form, errors could be shown like this.

<% if @article.errors.any? %>
    <div id="error_explanation">
        <h2>
        <%= pluralize(@article.errors.count, "error") %> prohibited
        this article from being saved:
        </h2>
        <ul>
        <% @article.errors.full_messages.each do |msg| %>
            <li><%= msg %></li>
        <% end %>
        </ul>
    </div>
<% end %>

How do I show errors for the comments form? When I submit a comment without a commentor, it does not save, so validations are happening, but I'm not sure how to show errors for this type of form.

<% if @???????.errors.any? %> ###### What do I say here to get the comment errors?
    <div id="error_explanation">
        <h2>
        <%= pluralize(@?????.errors.count, "error") %> prohibited
        this article from being saved:
        </h2>
        <ul>
        <% @?????.errors.full_messages.each do |msg| %>
            <li><%= msg %></li>
        <% end %>
        </ul>
    </div>
<% end %>
1 Answers

1. Create a reusable partial for errors

# app/views/shared/_errors.html.erb
<div class="error_explanation">
  <h2><%= pluralize(object.errors.count, "error") %> prohibited
 this <%= object.model_name.singular %> from being saved:</h2>
  <ul>
  <% object.errors.full_messages.each do |msg| %>
    <li><%= msg %></li>
  <% end %>
  </ul>
</div>

And a little helper method:

# app/helpers/application_helper.rb
module ApplicationHelper
  # Displays the errors for a model instance if there are any
  def display_errors_for(object)
    return unless object.errors.any?
    render partial: 'shared/errors', 
           locals: { object: object }
  end
end

2. Get the object from the form builder

You can always access the model wrapped by the form builder instance through the #object method instead of using a instance variable:

<%= form_with(model: [ @article, @comment ], local: true) do |form| %>
  # ...
  <%= display_errors_for(form.object) %>
<% end %>

And like magic you can add errors to any form with a single line.

Do not use @article.comments.build. That will always bind the form to a new instance of comment instead of displaying the errors! It will also remove anything the user entered into the form... Assign the variable in the controller. I have no idea how this snuck its way into the guides.

class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    @comment = @article.comments.new
  end
end
Related