Nested registration form does not display error messages for nested object
Organization
and User
are related many-to-many
through Relationship
. There is a combined registration form. The registration form works in that the valid information is saved, and if there is invalid information, it rolls back everything.
The problem is that the form is not displaying error messages for the nested object User
. Errors are displayed for Organization
, the form overrides correctly if there are errors for User
, but errors for are User
not displayed.
Why are there no errors when submitting incorrect information for users
? Any help is appreciated.
Registration form / type:
<%= form_for @organization, url: next_url do |f| %>
<%= render partial: 'shared/error_messages', locals: { object: f.object, nested_models: f.object.users } %>
... fields for organization...
<%= f.fields_for :users do |p| %>
...fields for users...
<% end %>
<%= f.submit "Register" %>
<% end %>
Common error messages:
<% if object.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(object.errors.count, "error") %>.
</div>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<% if defined?(nested_models) && nested_models.any? %>
<div id="error_explanation">
<ul>
<% nested_models.each do |nested_model| %>
<% if nested_model.errors.any? %>
<ul>
<% nested_model.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
<% end %>
<% end %>
</ul>
</div>
<% end %>
Controller method:
def new
@organization = Organization.new
@user = @organization.users.build
end
def create
@organization = Organization.new(new_params.except(:users_attributes))
@organization.transaction do
if @organization.valid?
@organization.save
begin
@user = @organization.users.create!(users_attributes)
@relationship = @organization.relationships.where(user: @user).first
@relationship.update_attributes!(member: true, moderator: true)
rescue
raise ActiveRecord::Rollback
end
end
end
if @organization.persisted?
if @organization.relationships.where('member = ? ', true).any?
@organization.users.where('member = ? ', true).each do |single_user|
single_user.send_activation_email
end
end
flash[:success] = "A confirmation email is sent."
redirect_to root_url
else
@user = @organization.users.build(users_attributes) if @organization.users.blank?
render :new
end
end
Organization model:
has_many :relationships, dependent: :destroy
has_many :users, through: :relationships, inverse_of: :organizations
accepts_nested_attributes_for :users, :reject_if => :all_blank, :allow_destroy => true
validates_associated :users
Relationship model:
belongs_to :organization
belongs_to :user
User Model:
has_many :relationships, dependent: :destroy
has_many :organizations, through: :relationships, inverse_of: :users
Update: . If you add an extra line in def create
like below, it works, that is, it displays error messages. However, for some reason, it does not keep accurate information when sending. Any ideas how to deal with this?
def create
@organization = Organization.new(new_params.except(:users_attributes))
@user = @organization.users.new(users_attributes)
@organization.transaction do
...
source to share
Maybe try this:
<%= render partial: 'shared/error_messages',
locals: { object: f.object, nested_models: [ @user ] } %>
My guess is that the call @organization.users.blank?
does not work as you expected because the user is not created correctly, because #create! threw exemption. Rails is probably doing a check on the database to see if there are currently users and thinks there is nothing there. This way your call gets @organization.users.build(users_attributes)
called, but that doesn't trigger validation.
In general, I would also recommend using a form object (like in another answer) when creating complex forms, as it makes things like this clearer and makes the view cleaner.
source to share
This is the classic use case for form objects. It is convenient from many points of view (testing, maintenance ...). For example:
class Forms::Registration
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
def persisted?
false
end
def initialize(attributes = {})
%w(name other_attributes).each do |attribute|
send("#{attribute}=", attributes[attribute])
end
end
validates :name, presence: true
validate do
[user, organization].each do |object|
unless object.valid?
object.errors.each do |key, values|
errors[key] = values
end
end
end
end
def user
@user ||= User.new
end
def organization
@organization ||= Organization.new
end
def save
return false unless valid?
if create_objects
# after saving business logic
else
false
end
end
private
def create_objects
ActiveRecord::Base.transaction do
user.save!
organization.save!
end
rescue
false
end
end
controller:
class RegistrationsController < ApplicationController
def new
@registration = Forms::Registration.new
end
def create
@registration = Forms::Registration.new params[:registration]
if @registration.save
redirect_to root_path
else
render action: :new
end
end
end
and representation in HAML:
= form_for @registration, url: registrations_path, as: :registration do |f|
= f.error_messages
= f.label :name
= f.text_field :name
= f.submit
It is worth reading more about form objects .
source to share
The nested attributes bit me SOOO hard every time I decide this is the right time to use them and I see you know a little about what I'm talking about.
Here's a different approach suggested, use a form object instead of nested attributes: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ see section3. Extract Form Objects
You can extract the existing validations in the model User
into a module and import this to deploy the solution from the blog:
https://gist.github.com/bbozo/50f8638787d6eb63aff4
With this approach, you can make your controller code super-simple and make simple and fast unit tests of the less-than-simple logic that you inject internally, and save yourself the hassle of writing integration tests to test all possible scenarios.
Also, you might find that a bunch of validations in the custom model are actually only within the registration form and that these checks will come and bite into more complex forms, especially if you are importing data from a legacy application where the checks weren't that strict. , or when you one day add additional validators and don't do half of your custom entries to update.
source to share
I had a similar problem. everything seemed to work fine, but I didn't get any errors. The solution I found was to create a comment in the # show article instead of a view:
@article = Article.find(params[:id])
@comment = @article.comments.build(params[:comment])
and in your articles do not use # show @article.comments.build
, but @comment
:
<%= form_for([@article, @comment]) do |f| %>
<%= render 'shared/error_messages', :object => f.object %>
<p><%= f.submit %></p>
<% end %>
make sure you create a comment in your # create comment (also you really have no choice: P)
I think you need to pass f.object
instead @comment
.
source to share