Multiple objects in a Rails form Multiple objects in a Rails form ruby-on-rails ruby-on-rails

Multiple objects in a Rails form


In Rails 4, just this

<%= form_tag photos_update_path do %>  <% @photos.each do |photo| %>     <%= fields_for "photos[]", photo do |pf| %>      <%= pf.text_field :caption %>      ... other photo fields


UPDATE: This answer applies to Rails 2, or if you have special constraints that require custom logic. The easy cases are well addressed using fields_for as discussed elsewhere.

Rails isn't going to help you out a lot to do this. It goes against the standard view conventions, so you'll have to do workarounds in the view, the controller, even the routes. That's no fun.

The key resources on dealing with multi-model forms the Rails way are Stephen Chu's params-foo series, or if you're on Rails 2.3, check out Nested Object Forms

It becomes much easier if you define some kind of singular resource that you are editing, like a Photoset. A Photoset could be a real, ActiveRecord type of model or it can just be a facade that accepts data and throws errors as if it were an ActiveRecord model.

Now you can write a view form somewhat like this:

<%= form_for :photoset do |f|%>  <% f.object.photos.each do |photo| %>    <%= f.fields_for photo do |photo_form| %>      <%= photo_form.text_field :caption %>      <%= photo_form.label :caption %>      <%= photo_form.file_field :attached %>    <% end %>  <% end %><% end %>

Your model should validate each child Photo that comes in and aggregate their errors. You may want to check out a good article on how to include Validations in any class. It could look something like this:

class Photoset  include ActiveRecord::Validations  attr_accessor :photos  validate :all_photos_okay  def all_photos_okay    photos.each do |photo|      errors.add photo.errors unless photo.valid?    end  end  def save    photos.all?(&:save)  end  def photos=(incoming_data)    incoming_data.each do |incoming|       if incoming.respond_to? :attributes         @photos << incoming unless @photos.include? incoming       else         if incoming[:id]            target = @photos.select { |t| t.id == incoming[:id] }         end         if target            target.attributes = incoming         else            @photos << Photo.new incoming          end       end    end  end  def photos     # your photo-find logic here    @photos || Photo.find :all  endend

By using a facade model for the Photoset, you can keep your controller and view logic simple and straightforward, reserving the most complex code for a dedicated model. This code probably won't run out of the box, but hopefully it will give you some ideas and point you in the right direction to resolve your question.


Rails does have a way to do this - I don't know when it was introduced, but it's basically described here: http://guides.rubyonrails.org/form_helpers.html#using-form-helpers

It took a bit of fiddling to alter the configuration properly for the case where there's no parent object, but this seems to be correct (it's basically the same as gamov's answer, but cleaner and doesn't allow for "new" records mixed in with the "update" records):

<%= form_tag photos_update_path do %>  <% @photos.each do |photo| %>     <%= fields_for "photos[#{photo.id}]", photo do |pf| %>      <%= pf.text_field :caption %>        ... [other fields]    <% end %>  <% end %><% end %>

In your controller, you'll end up with a hash in params[:photos], where the keys are photo IDs, and the values are attribute hashes.