Rails Form Object with Virtus: has_many association - ruby-on-rails

I am having a tough time figuring out how to make a form_object that creates multiple associated objects for a has_many association with the virtus gem.
Below is a contrived example where a form object might be overkill, but it does show the issue I am having:
Lets say there is a user_form object that creates a user record, and then a couple associated user_email records. Here are the models:
# models/user.rb
class User < ApplicationRecord
has_many :user_emails
end
# models/user_email.rb
class UserEmail < ApplicationRecord
belongs_to :user
end
I proceed to create a a form object to represent the user form:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
include Virtus.model
attribute :name, String
attribute :emails, Array[EmailForm]
validates :name, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
puts "I would proceed to create all the necessary objects by hand"
# user = User.create(name: name)
# emails.each do |email_form|
# UserEmail.create(user: user, email: email_form.email_text)
# end
end
end
One will notice in the UserForm class that I have the attribute :emails, Array[EmailForm]. This is an attempt to validate and capture the data that will be persisted for the associated user_email records. Here is the Embedded Value form for a user_email record:
# app/forms/email_form.rb
# Note: this form is an "Embedded Value" Form Utilized in user_form.rb
class EmailForm
include ActiveModel::Model
include Virtus.model
attribute :email_text, String
validates :email_text, presence: true
end
Now I will go ahead and show the users_controller which sets up the user_form.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
#user_form = UserForm.new
#user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to #user, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails: [:email_text]})
end
end
The new.html.erb:
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
And the _form.html.erb:
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<% unique_index = 0 %>
<% f.object.emails.each do |email| %>
<%= label_tag "user_form[emails][#{unique_index}][email_text]","Email" %>
<%= text_field_tag "user_form[emails][#{unique_index}][email_text]" %>
<% unique_index += 1 %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Note: If there is an easier, more conventional way to display the inputs for the user_emails in this form object: let me know. I could not get fields_for to work. As shown above: I had to write out the name attributes by hand.
The good news is that the form does render:
The html of the form looks ok to me:
When the above input is submitted: Here is the params hash:
Parameters: {"utf8"=>"✓", "authenticity_token"=>”abc123==", "user_form"=>{"name"=>"neil", "emails"=>{"0"=>{"email_text"=>"foofoo"}, "1"=>{"email_text"=>"bazzbazz"}, "2"=>{"email_text"=>""}}}, "commit"=>"Create User form"}
The params hash looks ok to me.
In the logs I get two deprecation warnings which makes me think that virtus might be outdated and thus no longer a working solution for form objects in rails:
DEPRECATION WARNING: Method to_hash is deprecated and will be removed in Rails 5.1, as ActionController::Parameters no longer inherits from hash. Using this deprecated behavior exposes potential security problems. If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (called from new at (pry):1)
DEPRECATION WARNING: Method to_a is deprecated and will be removed in Rails 5.1, as ActionController::Parameters no longer inherits from hash. Using this deprecated behavior exposes potential security problems. If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (called from new at (pry):1)
NoMethodError: Expected ["0", "foofoo"} permitted: true>] to respond to #to_hash
from /Users/neillocal/.rvm/gems/ruby-2.3.1/gems/virtus-1.0.5/lib/virtus/attribute_set.rb:196:in `coerce'
And then the whole thing errors out with the following message:
Expected ["0", <ActionController::Parameters {"email_text"=>"foofoo"} permitted: true>] to respond to #to_hash
I feel like I am either close and am missing something small in order for it to work, or I am realizing that virtus is outdated and no longer usable (via the deprecation warnings).
Resources I looked at:
this article.
this video
I did attempt to get the same form to work but with the reform-rails gem. I ran into an issue there too. That question is posted here.
Thanks in advance!

I would just set the emails_attributes from user_form_params in the user_form.rb as a setter method. That way you don't have to customize the form fields.
Complete Answer:
Models:
#app/modeles/user.rb
class User < ApplicationRecord
has_many :user_emails
end
#app/modeles/user_email.rb
class UserEmail < ApplicationRecord
# contains the attribute: #email
belongs_to :user
end
Form Objects:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
include Virtus.model
attribute :name, String
validates :name, presence: true
validate :all_emails_valid
attr_accessor :emails
def emails_attributes=(attributes)
#emails ||= []
attributes.each do |_int, email_params|
email = EmailForm.new(email_params)
#emails.push(email)
end
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.new(name: name)
new_emails = emails.map do |email|
UserEmail.new(email: email.email_text)
end
user.user_emails = new_emails
user.save!
end
def all_emails_valid
emails.each do |email_form|
errors.add(:base, "Email Must Be Present") unless email_form.valid?
end
throw(:abort) if errors.any?
end
end
# app/forms/email_form.rb
# "Embedded Value" Form Object. Utilized within the user_form object.
class EmailForm
include ActiveModel::Model
include Virtus.model
attribute :email_text, String
validates :email_text, presence: true
end
Controller:
# app/users_controller.rb
class UsersController < ApplicationController
def index
#users = User.all
end
def new
#user_form = UserForm.new
#user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
end
end
Views:
#app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
#app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<%= f.fields_for :emails do |email_form| %>
<div class="field">
<%= email_form.label :email_text %>
<%= email_form.text_field :email_text %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>

You have an issue because you haven't whitelisted any attributes under :emails. This is confusing, but this wonderful tip from Pat Shaughnessy should help set you straight.
This is what you're looking for, though:
params.require(:user_form).permit(:name, { emails: [:email_text, :id] })
Note the id attribute: it's important for updating the records. You'll need to be sure you account for that case in your form objects.
If all this form object malarkey with Virtus gets to be too much, consider Reform. It has a similar approach, but its raison d'etre is decoupling forms from models.
You also have an issue with your form… I'm not sure what you were hoping to achieve with the syntax you're using, but if you look at your HTML you'll see that your input names aren't going to pan out. Try something more traditional instead:
<%= f.fields_for :emails do |ff| %>
<%= ff.text_field :email_text %>
<% end %>
With this you'll get names like user_form[emails][][email_text], which Rails will conveniently slice and dice into something like this:
user_form: {
emails: [
{ email_text: '...', id: '...' },
{ ... }
]
}
Which you can whitelist with the above solution.

The problem is that the format of the JSON being passed to UserForm.new() is not what is expected.
The JSON that you are passing to it, in the user_form_params variable, currently has this format:
{
"name":"testform",
"emails":{
"0":{
"email_text":"email1#test.com"
},
"1":{
"email_text":"email2#test.com"
},
"2":{
"email_text":"email3#test.com"
}
}
}
UserForm.new() is actually expecting the data in this format:
{
"name":"testform",
"emails":[
{"email_text":"email1#test.com"},
{"email_text":"email2#test.com"},
{"email_text":"email3#test.com"}
}
}
You need to change the format of the JSON, before passing it to UserForm.new(). If you change your create method to the following, you won't see that error anymore.
def create
emails = []
user_form_params[:emails].each_with_index do |email, i|
emails.push({"email_text": email[1][:email_text]})
end
#user_form = UserForm.new(name: user_form_params[:name], emails: emails)
if #user_form.save
redirect_to #user, notice: 'User was successfully created.'
else
render :new
end
end

Related

Form Objects in Rails

The example code below is a contrived example of an attempt at a form object where it is probably overkill to utilize a form object. Nonetheless: it shows the issue I am having:
I have two models: a User and an Email:
# app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
# app/models/user.rb
class Email < ApplicationRecord
belongs_to :user
end
I want to create a form object which creates a user record, and then creates three associated email records.
Here are my form object classes:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :email_forms
validates :name, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
puts "I would proceed to create all the necessary objects by hand"
user = User.create(name: name)
email_forms.each do |email|
Email.create(user: user, email_text: email.email_text)
end
end
end
# app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
# DON'T THINK I WOULD PERSIST DATA HERE
# INSTEAD DO IT IN THE user_form
end
end
Notice: the validations on the form objects. A user_form is considered to be invalid if it's name attribute is blank, or if the email_text attribute is left blank for any of the email_form objects inside it's email_forms array.
For brevity: I will just be going through the new and create action of utilizing the user_form:
# app/controllers/user_controller.rb
class UsersController < ApplicationController
def new
#user_form = UserForm.new
#user_form.email_forms = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {email_forms: [:_destroy, :id, :email_text, :user_id]})
end
end
Lastly: the form itself:
# app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
<%= link_to 'Back', users_path %>
# app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
# MESSY, but couldn't think of a better way to do this...
<% unique_index = 0 %>
<% user_form.email_forms.each do |email_form| %>
<div class="field">
<%= label_tag "user_form[email_forms][#{unique_index}][email_text]", "Email Text" %>
<%= text_field_tag "user_form[email_forms][#{unique_index}][email_text]" %>
</div>
<% unique_index += 1 %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
The form does render:
And here is the form's html:
I go to submit the form. Here is the params hash:
Parameters: {"utf8"=>"✓", "authenticity_token"=>”abc123==", "user_form"=>{"name"=>"neil", "email_forms"=>{"0"=>{"email_text"=>"test_email_1"}, "1"=>{"email_text"=>"test_email_2"}, "2"=>{"email_text"=>""}}}, "commit"=>"Create User form"}
What should happen is the form should be re-rendered and nothing persisted because the form_object is invalid: All three associated emails must NOT be blank. However: the form_object thinks it is valid, and it blows up in the persist! method on the UserForm. It highlights the Email.create(user: user, email_text: email.email_text) line and says:
undefined method `email_text' for ["0", {"email_text"=>"test_email_1"}]:Array
Clearly there are a couple things going on: The nested validations appear to not be working, and I am having trouble rebuilding each of the emails from the params hash.
Resources I have already examined:
This Article seemed promising but I was having trouble getting it to work.
I have attempted an implementation with the virtus gem and the reform-rails gem. I have pending questions posted for both of those implementations as well: virtus attempt here and then reform-rails attempt here.
I have attempted plugging in accepts_nested_attributes, but was having trouble figuring out how to utilize that with a form object, as well as a nested form object (like in this code example). Part of the issue was that has_many and accepts_nested_attributes_for do not appear to be included in ActiveModel::Model.
Any guidance on getting this form object to do what is expected would be very much appreciated! Thanks!
Complete Answer
Models:
#app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
#app/models/email.rb
class Email < ApplicationRecord
belongs_to :user
end
Controller:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
#users = User.all
end
def new
#user_form = UserForm.new
#user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
end
end
Form Objects:
#app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :emails
validates :name, presence: true
validate :all_emails_valid
def emails_attributes=(attributes)
#emails ||= []
attributes.each do |_int, email_params|
email = EmailForm.new(email_params)
#emails.push(email)
end
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.new(name: name)
new_emails = emails.map do |email_form|
Email.new(email_text: email_form.email_text)
end
user.emails = new_emails
user.save!
end
def all_emails_valid
emails.each do |email_form|
errors.add(:base, "Email Must Be Present") unless email_form.valid?
end
throw(:abort) if errors.any?
end
end
app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
end
Views:
app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
<%= link_to 'Back', users_path %>
#app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<%= f.fields_for :emails do |email_form| %>
<div class="field">
<%= email_form.label :email_text %>
<%= email_form.text_field :email_text %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>

Nested sign up form does not display the error messages for the nested object

Organization and User have a many-to-many relationship through Relationship. There's a joined signup form. The sign up form works in that valid information is saved while if there's invalid information it rolls back everything.
The problem is that the form does not display the error messages for the nested User object. Errors for Organization are displayed, the form correctly re-renders if there are errors for User, but the errors for User are not displayed.
Why are the errors when submitting invalid information for users not displayed? Any help is appreciated.
The signup form/view:
<%= 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 %>
The shared error messages partial:
<% 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 %>
The 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
The 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
The Relationship model:
belongs_to :organization
belongs_to :user
The User model:
has_many :relationships, dependent: :destroy
has_many :organizations, through: :relationships, inverse_of: :users
Update: If I add an additional line to def create as below, it seems to work, i.e., then it does display the error messages. However, then it for some reason doesn't save when valid information is submitted. Any ideas how to deal with that?
def create
#organization = Organization.new(new_params.except(:users_attributes))
#user = #organization.users.new(users_attributes)
#organization.transaction do
...
Maybe try this:
<%= render partial: 'shared/error_messages',
locals: { object: f.object, nested_models: [ #user ] } %>
I guess the call to #organization.users.blank? doesn't work in the way you expected it to do, as the user is not correctly created, because #create! threw an exeption. Rails probably does a check on the database, to see if there are any users now, and thinks there is still nothing in there. So your #organization.users.build(users_attributes) gets called, but this doesn't trigger validation.
In general I would also recommend you the use of a form object (like in the other answer), when creating complex forms, as this clarifies things like that and makes the view more clean.
This is classic use case for form objects. It is convenient from many perpectives (testing, maintainance ...).
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
the 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 the view 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 to read more about form objects.
Nested attributes bit me SOOO hard every time I decided it's a good time to use them, and I see you know a bit of what I'm talking about.
Here's a suggestion of a different approach, use a form object instead of nested attributes: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ see under section 3. Extract Form Objects
You can extract your existing validations on the User model into a module and import that, to expand on 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 not-so-simple logic that you implemented inside and save yourself from writing integration tests to test out all different possible scenarios.
Also, you might find out that a bunch of the validations in the user model are actually only within the concern of the signup form and that those validations will come and bite in later complex forms, especially if you're importing data from a legacy application where validations weren't so strict, or when you one day add additional validators and make half of your user records invalid for update.
I had a similar problem. everything seemed to work fine, but I was not getting any errors The solution i found is to build the comment in article#show instead of the view:
#article = Article.find(params[:id])
#comment = #article.comments.build(params[:comment])
and in your articles#show don't use #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 build the comment in your comment#create as well (you really have no choice though :P)
I think you need to pass f.object instead of #comment.
In case someone might be looking for a solution to render form errors in a form, try:
f.object.errors["account.address"].present?`
The address is the nested attribute here.

How to validate multiple models in a single transaction?

In my rails (4.1.6) app, I have a contact model that has_one :address, :email
I construct a contact and related address and email in a single form using fields_for:
views/contacts/new.html.erb
<%= form_for #contact, ... %>
...
<%= fields_for :address do |address_fields| %>
<%= address_fields.text_field :street, ... %>
<%= address_fields.text_field :city, ... %>
...
<% end %>
<%= fields_for :email do |email_fields| %>
<%= email_fields.text_field :display_name, ... %>
<%= email_fields.text_field :mail_id, ... %>
<% end %>
...
<% end %>
I want email to be required, while address is optional. In other words, if email is not provided, none of the 3 models should be created, but if only email is provided, the email and contact must be created.
One way that does work is to validate the params manually in the contacts_controller#create before constructing anything, and flash[:error] and return without saving if email is not specified, or save it if all is well:
contacts_controller.rb
def create
#contact = Contact.new
if(params_email_valid? params)
#contact.save!
#email = Email.create(...)
#email.save!
...
else
flash[:error] = 'Email must be specified to save a contact'
redirect_to :root
end
end
private:
def params_email_valid? params
!(params[:email][:display_name].blank? || params[:email][:mail_id].blank?)
end
Another way that may work is to drop down to SQL and validate everything through direct SQL calls in a transaction.
However, both of these are not 'the rails way', since validations belong in the models. So, I am trying to use some combination of validates_presence_of, validates_associated and custom validators to validate this scenario. The problem here is that model level validation of associated models requires either self to be already saved in the database, or the associated model to be already saved in the database. Is there a way to validate all these models in a single transaction?
Considering you have appropriate validations in the models:
class Contact <
has_many :addresses
has_many :emails
#add
accepts_nested_attributes_for :addresses, :emails #you can add some validations here to like reject_all if blank? see the docs
end
class Address <
belongs_to :contact
end
class Email <
belongs_to :contact
end
In your CompaniesController
def new
#contact = Contact.new
#contact.addresses.new
#contact.emails.new
end
def create
#contact = Contact.new(contact_params)
if #contact.save
#redirect add flash
else
#add flash
#render action: new
end
protected
def contact_params
#permit(#contact_fields, address_attributes: [#address_fields], email_attributes: [#email_fields])
end
And you would like to modify your form like this
<%= form_for #contact, ... do|f| %>
...
<%= f.fields_for :address do |address_fields| %>
<%= address_fields.text_field :street, ... %>
<%= address_fields.text_field :city, ... %>
...
<% end %>
<%= f.fields_for :email do |email_fields| %>
<%= email_fields.text_field :display_name, ... %>
<%= email_fields.text_field :mail_id, ... %>
<% end %>
...
<% end %>
So accepts_nested_attributes helps you validate the child as well as the parent and adds [child]_attributes getters and setters, So normally in your form what was contact[email][display_name] will become contact[email_attributes][display_name]

Rails Nested Form Not Saving Nested Attributes

I have the following User controller:
class UsersController < ApplicationController
def index
#users = User.all
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
#customer = Customer.new
if #user.save
flash.notice = "User '#{#user.email}' was succefully created."
redirect_to user_path(#user)
else
render 'new'
end
end
def show
#user = User.find(params[:id])
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation, customer_attributes: [:id, :company])
end
end
And I have the following User model:
class User < ActiveRecord::Base
has_one :customer
accepts_nested_attributes_for :customer, :allow_destroy => true
end
And the following Customer model:
class Customer < ActiveRecord::Base
belongs_to :user
end
Finally, here is the form:
<%= form_for [#user] do |f| %>
<% if #user.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% #user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :email %><br>
<%= f.text_field :email %>
</div>
<div class="field">
<%= f.label :password %><br>
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br>
<%= f.password_field :password_confirmation %>
</div>
<%= f.fields_for :customers do |company| %>
<div class="field">
<%= company.label :company %><br>
<%= company.text_field :company %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
When I submit the form, I see: `Unpermitted parameters: customers' in the log but it appears that I m indeed permitting it.
Also, I want to show the company name for each user in the show and index views. I'm not sure how to do that.
I remember using the build method in the past to get something similar to work but I can't seem to figure it out this time.
Further to #Mandeep's answer, let me give you some further information:
You need to "build" your associated objects for your form
You need to process this as per the association your model has
You need to save the attributes as per said association
The way to do this is relatively simple (outlined by Mandeep). However, the reason why might be a little less obvious:
Build
First, you need to build your associative association. This is vitally important, primarily because Rails (by virtue of being built on Ruby), is an object orientated framework.
Object orientation, without getting into too much detail, means that everything you do with Rails is going to be based around objects. In the case of our beloved Rails, it means that every Model is an object.
By virtue of this fact, the nested model paradigm has to be built in Rails whenever you want to create such a form. To do this, you need to use the build methods - which tell ActiveRecord (Rails' object relational mapper) that you have another associated model / object which you want to populate:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
#user = User.new #-> initializes "User" object
#user.build_customer #-> "builds" the associated object
end
end
This gives Rails a set of associated data which it can populate with your form (considering you call the correct methods)
--
Association
Second, you need to consider the association you have. This is important as singular & multiple associations are handled differently in the "build" process.
You're using a has_one relationship, which means you need to use singular association names (although you can call the associations whatever you want):
If you used a has_many association, you'd need to use the plural association methods:
This explains the need to use the build_customer method; but also should give you the presidence to use the singular association name for all the methods you need to get this working, namely fields_for and params:
#app/views/users/new.html.erb
<%= form_for #user do |f| %>
...
<%= f.fields_for :customer do |c| %>
...
<% end %>
<%= f.submit %>
<% end %>
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new user_params
#user.save
end
private
def user_params
params.permit(:user).permit(:user, :params, customer_attributes: [:x. :y])
end
end
--
Save
The above controller code will save the attributes you require.
You must understand that passing nested attributes means that the model you're sending the associative data to needs to be subordinated to your "main" model. This happens with the ActiveRecord associations in your models, as discussed initially.
Hopefully this gives you some more clarity
Change your code to this:
def new
#user = User.new
#user.build_customer
end
your form:
<%= form_for #user do |f| %>
// user fields
<%= f.fields_for :customer do |customer| %>
// customer fields
<% end %>
<% end %>
Also there is not need of #customer = Customer.new in your create method.

Virtus: Replace accepts_nested_attributes (one-to-many) with a form object

Since more than a month I try to get behind the secrets of form objects in Rails 4.
Using virtus, I am already able to build very simple forms. However, I fail to develop a form object that replaces accepts_nested_attributes_for (in the model) and fields_for (in the form view).
In this question I explain a small phonebook-example: the form provides the possibility to enter a person's name and 3 phone numbers at once (find the whole code here).
Now I try to do the same with a form object. I get as far as this:
# forms/person_form_new.rb
class PersonFormNew
class PhoneFormNew
include Virtus
include ActiveModel::Model
attr_reader :phone
attribute :phone_number, String
end
include Virtus
include ActiveModel::Model
attr_reader :person
attribute :person_name, String
attribute :phone, PhoneFormNew
def persisted?
false
end
def save
if valid?
persist
true
else
false
end
end
private
def persist
#person = Person.create(name: person_name)
#person.phones.build(:phone)
end
end
# views/people/new.html.erb
<h1>New Person</h1>
<%= form_for #person_form, url: people_path do |f| %>
<p>
<%= f.label :person_name %> </ br>
<%= f.text_field :person_name %>
</p>
<p>
<%= f.fields_for :phone do |f_pho| %>
<%= f_pho.label :phone_number %> </ br>
<%= f_pho.text_field :phone_number %>
<% end %>
<p>
<%= f.submit %>
</p>
<% end %>
This gives me the error
undefined method `stringify_keys' for :phone:Symbol
line: #person.phones.build(:phone)
I fear however, this is not the only error.
Can you point me the way to realize a one-to-many assignment with a form object (preferable using Virtus)?
One solution is to create the associated object in a separate function on the form model. I was succussful by doing the following:
def persist!
#user.save!
#account.save!
create_admin_membership
end
def create_admin_membership
#membership = Membership.create! do |membership|
membership.user = #user
membership.account = #account
membership.admin = true
end
end
You can find an extended explanation here: http://w3facility.org/question/how-to-create-another-object-when-creating-a-devise-user-from-their-registration-form-in-rails/

Resources