Nested forms with ActiveModel::Model objects
ActiveModel::Model is an excellent way to make objects behave like ActiveRecord. One potential use for this is if we want to make use of form data that doesn't necessarily persist to an object, but we want to keep our controllers clean. A great example can be found on this thoughtbot blog post from about a year ago.
One thing that I think many have found lacking, however, is has_many
and accepts_nested_attributes_for
functionality. I found myself in this boat recently when I wanted to use these form objects to create multiple records. How can I have a form that can create arbitrarily many records and have validation errors appear for each record I'm trying to create, without associations?
Like many, I began looking for gems to solve my problem, but after a day or so of searching for the perfect solution, I found the missing key. If an object with a one-to-many association is instantiated with a params hash, and that hash has a key for the association, Rails will call the <association_name>_attributes=
method on that object. So, we need to define that on our form object.
class ContactListForm
include ActiveModel::Model
attr_accessor :contacts
def contacts_attributes=(attributes)
@contacts ||= []
attributes.each do |i, contact_params|
@contacts.push(Contact.new(contact_params))
end
end
end
Now we need a form for this model. Keeping it simple:
<div>
<%= form_for @contact_list, url: contacts_path, method: :post do |f| %>
<%= f.fields_for :contacts do |c| %>
<%= c.text_field :first_name %>
<%= c.text_field :last_name
<%= c.text_field :phone %>
<% end %>
<p>
<%= f.submit "Submit" %>
</p>
<% end %>
</div>
Of course our object doesn't have any contacts yet, so our controller will need to make sure that the form has at least one fields_for
block to render by giving it one on initialization
class ContactsController < ApplicationController
def new
@contact_list = ContactListForm.new(contacts: [Contact.new])
end
end
My controller also needs a create
action for when the form is submitted.
class ContactsController < ApplicationController
def create
@contact_list = ContactListForm.new(params[:contact_list_form])
if @contact_list.save
flash[:notice] = "Created contacts"
redirect_to root_path
else
flash[:notice] = "There were errors"
render :action => 'new'
end
end
end
If all forms were valid, then all contacts were submitted to the database. But what about validation errors for each contact on the form? First I took some good advice from a StackOverflow post to just redefine the error_messages
method that was deprecated back in Rails 3
class CustomFormBuilder < ActionView::Helpers::FormBuilder
def error_messages
return unless object.respond_to?(:errors) && object.errors.any?
html = ""
html << @template.content_tag(:span, "Please correct the following errors.")
html << object.errors.full_messages.map { |message| @template.content_tag(:li, message) }.join("\n")
@template.content_tag(:ul, html.html_safe)
end
end
ActionView::Base.default_form_builder = CustomFormBuilder
Now just a small update to my form:
<%= f.fields_for :contacts do |c| %>
<%= c.error_messages %>
<%= c.text_field :first_name %>
<%= c.text_field :last_name
<%= c.text_field :phone %>
<% end %>
And I'm good to go.
For my next post, we'll add a link to the form that will add inputs for another contact, so that we may submit an arbitrary number of contacts. Please stay tuned!
Written by Sam Slotsky
Related protips
13 Responses
Great stuff. I assume you also implemented #save in your ContactListForm
but just left that out of the post?
Thank you!
Loved that article, helped me a lot, thanks.
This just saved my ass. I was about to nuke everything that I've been working on with a form object since Saturday morning. Literally the only example of a form object with a has_many association that I've found on the entire Interwebz.
Do you place validations in your model or in your form object? For example, let's say that each Contact has a number of validations. Where would those validations live? How would you access those validations from your ContactsListsForm?
Good question! If you include ActiveModel::Validations you can write the same validators as you would with ActiveRecord.
However, in this case, our form is just a collection of Contact objects, which are ActiveRecord and have their own validations. When I save the ContactListForm, it attempts to save all the contacts. In doing so, each contact has its error_messages available.
There is nothing wrong with accepts_nested_attributes_for
. This is what you should use in your typical case. My post describes a non-typical case. ContactListForm
is not an ActiveRecord
object, it is an object that includes ActiveModel::Model
, which does not support accepts_nested_attributes_for
.
How can I write a validations for nested resource? Contacts
, in this case.
Could you show how you have implemented the save
method
The validation could simply be contacts.all?(&:valid?)
. Contact
defines its own validation of course.
Thank you! You helped me a lot!
I have done what you have described, but just some name changes. When I submit the form, I get this error: ActiveModel::ForbiddenAttributesError in CampaignsController#create and this part is marked as the error: @campaigns.push(Campaign.new(campaign_params))
What have I done wrong?
Thanks in advanced!
Does this handle existing records like ActiveRecord does? I mean:
- Update records for attributes that include an id
attribute
- Create records for attributes that DON'T include an id
- Delete all the rest
Great stuff. Thanks. Interested for the next post :)