Topic: Creating Many Models in One Form

Earlier I showed you how to create two models in one form. In this article I will show you how to create many models (more than two) in a single form. Just like in the other article, project has many tasks.

In the new action we need to set up a  project with a few tasks. Let's say 5.

# in projects_controller.rb
def new
  @project = Project.new
  5.times { @project.tasks.build }
end

In the view we need to loop through the project's tasks and display the fields for each one:

# in projects/new.rhtml
<% for task in @project.tasks %>
  <p><%= text_field :task, :name %></p>
<% end %>

The problem with this is that every field will have the same name, so when we submit the form only one field will be sent. We need to find some way to identify each field to give it a unique name. An easy way to do this is to add the index of the array to the name of the field using the fields_for helper.

# in projects/new.rhtml
<% @project.tasks.each_with_index do |task, index| %>
  <% fields_for "tasks[#{index}]", task do |f| %>
    <p><%= f.text_field :name %></p>
  <% end %>
<% end %>

When the form is submitted, the values for the tasks will be in the params[:tasks] hash. We can loop through this to build the tasks for the project and save them to the database. Like this:

# in projects_controller.rb
def create
  @project = Project.new(params[:project])
  params[:tasks].each_value { |task| @project.tasks.build(task) }
  if @project.save
    redirect_to :action => 'index'
  else
    render :action => 'new'
  end
end

There is one more problem we need to solve. If the task is left blank, we don't want to add it to the project. We can check for blank tasks easily enough inside that build loop:

# in projects_controller.rb
params[:tasks].each_value do |task|
  @project.tasks.build(task) unless task.values.all?(&:blank?)
end

That's it. Here's the entire code:

# in projects_controller.rb
def new
  @project = Project.new
  5.times { @project.tasks.build }
end

def create
  @project = Project.new(params[:project])
  params[:tasks].each_value do |task|
    @project.tasks.build(task) unless task.values.all?(&:blank?)
  end
  if @project.save
    redirect_to :action => 'index'
  else
    render :action => 'new'
  end
end

# in projects/new.rhtml
<%= start_form_tag :action => 'create' %>
<p>Project Name: <%= text_field :project, :name %></p>
<h2>Tasks</h2>
<% @project.tasks.each_with_index do |task, index| %>
  <% fields_for "tasks[#{index}]", task do |f| %>
    <p><%= f.text_field :name %></p>
  <% end %>
<% end %>
<%= end_form_tag %>


In the next article I show you how to remove/add tasks dynamically in one form.

Last edited by ryanb (2006-11-06 02:22:48)

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Many Models in One Form

Thank you very much for this article... by chance do you have a blog with these and other articles?

Re: Creating Many Models in One Form

Sorry, I don't have a blog. All articles I have written are on this forum. Maybe some day I'll make my own site though.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Many Models in One Form

How would you save the form data if we introduced a new relation, People, where one person works on one task (ie. Task now has a :person_id attribute), and we want to use the Person's name when entering the Task data? I guess what I'm asking is, how do you go from having the Person's name in the POST data to adding this Person to the current task? I'm getting errors when I try sticking the Person's name into params[tasks][index][person_name], and I'm not sure how to associate tasks and people if I stick the Person's name into params[people][name]. Thanks in advance!

Re: Creating Many Models in One Form

This is easy to do if you want a select box where you have a list of people's names. You can then give the select box the name "person_id" so the id of the selected name is set to that attribute upon submit.

However, I'm assuming you want a text field where you insert a person's name? This is possible through what I call a fake attribute. The task doesn't have a "person_name" attribute, but we can make one by creating a getter/setter method for it in the task model:

# in Task
def person_name
  person.name unless person.nil?
end

def person_name=(name)
  self.person = Person.find_by_name(name)
end


Now the params[tasks][index][person_name] should work because this will treat it just like an attribute. The controller doesn't even know it's matching a person behind the scenes.

You may want to add your own error handling to check if the typed name actually matches a person.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Many Models in One Form

Is there any way to use "error_messages_for :tasks[x]" with this technique?

Last edited by jed.hurt (2007-03-06 20:17:41)

I thought about how mothers feed their babies with tiny little spoons and forks, so I wondered what do Chinese mothers use. Toothpicks?

Re: Creating Many Models in One Form

The error_messages_for method looks for an instance variable, so you have to set that before calling it. For example:

<% @project.tasks.each_with_index do |task, index| %>
  <% @task = task %>
  <%= errors_messages_for :task %>
  <% fields_for "tasks[#{index}]", task do |f| %>
    <p><%= f.text_field :name %></p>
  <% end %>
<% end %>

Kind of stupid I know, but that's how it works. I really recommend looping through the tasks error messages yourself and handling how it is displayed. This way you can present it to the user in a better manner.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Many Models in One Form

are there any good tutorials on looping through the error messages? in my model i have multiple tasks for example and the validation returns something like this:

Invalid Tasks
Invalid Tasks

thanks for helping a newb

Re: Creating Many Models in One Form

I'm not sure of any tutorials out there on looping through error messages. Every model has an errors object. It stores the error messages. You can loop through them like this:

<ul>
<% for message in @project.errors.full_messages %>
  <li><%= message %></li>
<% end %>
</ul>

Or something like that.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Many Models in One Form

ryanb,

Would it be possible to post an example of an update action using the form provided in this example? I am having trouble getting the correct params call to update the attributes.

Thank you,

Re: Creating Many Models in One Form

There is another tutorial for the edit/update action. The reason it needs to be separate is because that uses the "id" of the model to keep track of it, where as this uses the index of the model. I'm planning to look into this problem in the future and write a new tutorial on it, I just haven't gotten around to it.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Many Models in One Form

I see. That is the problem I was having. I saw the other tutorial but I was hoping to have one single _form.html file for both new and edit. Do you think you get to do the tutorial anytime soon?

Thank you for all the other work,

Last edited by cauta (2007-04-04 13:11:42)

Re: Creating Many Models in One Form

Hi all,

This is working great and I have also used the other tutorials of how to do edit/update actions too.

Now in the def new we add.

5.times { @project.tasks.build }

Which creates 5 fields for you to fill out. If the main form has errors these 5 fields simply disappear. I could add this code in the create def, but then if all was ok it would create 5 empty fields for the submission. Hope that makes sense?

I am using rails 1.2.3 and my app is RESTful

Cheers,
Steve

Last edited by stevepsharpe (2007-04-06 13:11:17)

Re: Creating Many Models in One Form

Actually the 5 fields stick around because of this line in the create action:

params[:tasks].each_value { |task| @project.tasks.build(task) }

This creates the tasks for the project just like in the "new" action, but creates them with the values submitted from the form. It should just work.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Many Models in One Form

Ah I missed that one sorry.

However I am having a weird behavior. Ignore the whole task thing. My main model is Property and I have Image as the secondary model (ie tasks).

Each image has a caption too. So the fields need to be a file field and a text field for the caption.

So if I choose an image and add a caption, but miss out any of the main model's fields for example city or street_name. It then duplicates the file and caption fields. By default I only add one set of fields not 5 as in the example. But if the main model has errors it shows 2 sets of fields, not 1 with the caption duplicated in both sets. If however I leave the file and caption blank and get errors on the main model it stays at just one set of fields for the image as expected.

Once again I hope this all makes sense?

Cheers,
Steve

Re: Creating Many Models in One Form

Can you post the code?

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Many Models in One Form

Sure, I seemed to have fixed it. I had some code in there that I think was kind of duplicating the code that you suggested I use. See the commented code.

def create
    @property = Property.new(params[:property])
    @property_type_list = PropertyType.find(:all, :order => 'name')
    params[:property_images].each_value { |property_image| @property.property_images.build(property_image) }
   
    # params[:property_images].each_value do |property_image|
    #   @property.property_images.build(property_image) unless property_image.values.all?(&:blank?)
    # end
     
    respond_to do |format|
     
    if @property.save 
        flash[:notice] = 'Property was successfully created.'
        format.html { redirect_to property_url(@property) }
        format.xml  { head :created, :location => property_url(@property) }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @property.errors.to_xml }
      end
    end
  end

Re: Creating Many Models in One Form

Yep, that was the problem. The primary difference between the commented code and my code is it will only build the property_image unless all of the form fields are blank. This is a good technique if you don't want to require they make a property_image the same time the Property is create. However, only do one or the other - not both.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Many Models in One Form

Hi again

I have another question about the building of form elements, like so.

@property.property_images.build

Like I have said before I have a main Property Model and PropertyImage model for all images (using attachment_fu). Properties can have unlimted images. This is all working fine, when you add a property you can add images on the fly with rjs. So min 1 - max whatever.

So on to the question, once you have submitted a property, I want a way to add new images to a property. I have this working but I would like to be able add fields on the fly again, starting with 1 field by default. If I use the code above it will create the amount of fields based on the number of already assigned images. So if a property had 10 it would make 10 fields. Which kind of makes sense.

I could do the following I guess?

<% 1.times do |index| %>
                <%= render :partial => 'image_fields', :locals => { :index => index, :new_position => @newimage_id } %>
            <% index += 1 %>
            <% end %>

But I am sure there is a cleaner way of doing this?

Any help is greatly appreciated as always.

Cheers,
Steve

Re: Creating Many Models in One Form

No sense in doing "1.times". You can just render it once:

<%= render :partial => 'image_fields', :locals => { :index => 0, :new_position => @newimage_id } %>

Not sure if that will work, but worth a try.

Railscasts - Free Ruby on Rails Screencasts