Topic: Creating Two Models in One Form

Q: How do I make a form that handles multiple models?

This question is asked quite frequently. Scaffolding and simple Rails tutorials show you how to create one model per form. Unfortunately, sometimes this is not sufficient.

In this article we will make one form that creates two models: Project and Task (where a project has many tasks). What I would like to do is have the first task be created the same time the project is created (in the same form). Here's the new action in the controller:

# in the projects_controller.rb
def new
  @project = Project.new
  @task = Task.new
end

Here we are creating the models which will contain the default values - we can set any default values in this action and they will be reflected in the form.

Next we make the form containing fields for these two models. Of course you could add as many fields as you need to for each model:

# in the projects/new.rhtml
<h1>New Project</h1>

<%= error_messages_for :project %>
<%= error_messages_for :task %>

<%= start_form_tag :action => 'create' %>
<p>
  Project Name:
  <%= text_field :project, :name %>
</p>
<p>
  First Task:
  <%= text_field :task, :name %>
</p>
<p>
  <%= submit_tag 'Create' %>
</p>
<%= end_form_tag %>


Pretty simple. Now comes the tricky part, the create action. First the code, then I'll explain it.

def create
  @project = Project.new(params[:project])
  @task = @project.tasks.build(params[:task])
  if @project.save
    redirect_to :action => 'index'
  else
    render :action => 'new'
  end
end

The @project instance variable is set normally, but what's up with how the task is created? We use a fancy method to help us out here. The @project.tasks.build method creates a task (just like Task.new) and adds it to the @project at the same time. Pretty cool huh?

Next we call @project.save. This method is actually doing a lot. First it checks if the project and all of its tasks are valid. If something is invalid it returns false, otherwise it saves both project and task and returns true. This save method is very convenient and handles everything for us automagically.

Why do we bother setting the @task instance variable you may wonder? We need it in case the validation fails and we render the 'new' form again.

The creation of the project along with the task is finished, but how about editing? Normally, with this kind of set up, I use separate forms for editing. In this case I would list the tasks on the project 'show' page with an edit link next to each one.

If you want to edit all of the tasks on one big form, check out the next article in this series.

Last edited by ryanb (2006-09-26 19:48:21)

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Two Models in One Form

Nice article! Just one thing: I prefer combining the new and the create action to a singel action. But I think thats just favor...

My homepage: http://www.komendera.com/
Working at: http://www.abloom.at/
My blog: soaked and soaped http://soakedandsoaped.com/

Re: Creating Two Models in One Form

Dieter Komendera wrote:

Nice article! Just one thing: I prefer combining the new and the create action to a singel action. But I think thats just favor...

I actually like that better too, but it's not compatible with Simply RESTful, so I've gone back to using two actions.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Two Models in One Form

Thank you Ryan for all your great Tutorials.

Re: Creating Two Models in One Form

As a Rails newbie, I just wanted to extend a large THANK YOU for this tutorial -- it has helped me immensely.

Re: Creating Two Models in One Form

Thanks for the tutorial for sure, clear and simple to follow.

I'm having a hard time though figuring out how to combine this with the RESTful stuff in edge rails (and SImply Restful).

I have a user, who has groups, with contacts in them. When I create a new contact, they choose what group to put the contact in, and also have the option of adding a new group.

To add a new group, in RESTful manner that should be a call to the create action on the groups_controller, and to add a new contact it should be a call to the create action on the contacts_controller. But I want it to all be in one call.

Do I have to break the RESTful model by folding group creation into contact creation, or can anybody suggest any other ways to do this!

Thanks in advance,

Cameron

Re: Creating Two Models in One Form

cameron wrote:

Do I have to break the RESTful model by folding group creation into contact creation, or can anybody suggest any other ways to do this!

I don't think you are breaking the REST convention by doing this. The important thing is that you have one primary model which you are creating/editing. You can add/update as many other models in the same form as you want as long as they are directly related to the primary model.

If it helps, think of updating/creating these secondary models as altering the attributes of the primary model. You could have been storing the group as a string in the contacts table (but that wouldn't be as good of a design). This is a little oversimplifying the problem, but I hope you grasp the concept. I like to think of REST as more of a best practice/convention and shouldn't be taken too strictly.

Something you do need to be concerned about is duplicate code. In my experience, creating multiple models in one form does not lead to duplicate code because you are setting up the relationship at the same time. For example, creating the group in the GroupsController would look like this:

@group = Group.new(params[:group])

In the ContactsController you create the group while setting up the relationship
@group = @contact.build_group(params[:group])

Does that make sense?

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Two Models in One Form

What about multiple tasks? And what if the tasks are related to the Project via has_many :through (ie. build won't work)?

Last edited by skwasha (2006-12-15 01:31:34)

Re: Creating Two Models in One Form

In that case you would probably place checkboxes or something in the Task edit page to assign it to given projects. There are other ways to do this as well. If you do a search for "HABTM checkbox" on the site you will probably find a couple posts on the subject. I may write another tutorial for this specific case as well.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Two Models in One Form

I seem to be doing something wrong.
I keep receiving this error when i submit:
undefined method `reviews' for #<Album:0x24f6c00>

Here is my code

def new
@album = Album.new
@artist = Artist.find(:all)
@review = Review.new
end

def create
@album = Album.new(params[:album])
@review = @album.reviews.build(params[:review])
if @album.save
  redirect_to :controller => 'main', :action => 'index'
else
  render :controller => 'albums', :action => 'new'
end
end

Last edited by ldenman (2006-12-25 02:46:45)

Re: Creating Two Models in One Form

Have you set up the association in the Album model?

class Album < ActiveRecord::Base
  has_many :reviews
end

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Two Models in One Form

Hi Ryan, thanks for the tutorial. Umm, one thing I want to learn is why do we use render instead of redirect_to when @project.save return false? Could you please explain a bit?

Re: Creating Two Models in One Form

Certainly. The reason is if the validation fails, the errors are added to the model object (in this case @project). All instance variables are lost on a redirect because it's like an entirely new request. This means the error messages are lost as well as the data the user typed into the form. This is why we just do a render so the errors and other data is still there and we can display it on the rendered page.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Two Models in One Form

Ahh, excellent. Thank you! big_smile

15

Re: Creating Two Models in One Form

I was just doing some reading and noticed that error_messages_for will take more than one object name so you can do:

<%= error_messages_for :project, :task %>

And all the errors will be rendered in the same div.

Last edited by bp (2007-01-27 11:15:41)

Re: Creating Two Models in One Form

What a great find! I don't remember this before. I wonder if it's new in 1.2.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Two Models in One Form

This is definitely a great tutorial !
I've a quick question.
Imagine you want each tasks of your project to be attached to this project. I think you will add
validates_presence_of :project_id within the task model.
However if you do that when there is a validation error you get two nasty messages not very user friendly :
Tasks is invalid
Project can't be blank
On a computer point of view these messages make total sense, however form the user side...
Is there someone knows how to hide validation messages which concerns database relations ?

Thanks
Cyrille

Last edited by scyrille (2007-02-03 16:34:52)

Re: Creating Two Models in One Form

Not sure the best way to solve this. You an try setting the :message parameter to nil when setting up the validation.

validates_presence_of :project_id, :message => nil

Untested. No clue of that will solve the problem.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Two Models in One Form

scyrille wrote:

However if you do that when there is a validation error you get two nasty messages not very user friendly :
Tasks is invalid
Project can't be blank
On a computer point of view these messages make total sense, however form the user side...
Is there someone knows how to hide validation messages which concerns database relations ?

You could use conditional validation, e.g.:

validates_presence_of :task, :if => Proc.new { [v| !v.project.blank? }

This means that the validation rule for the task only gets run if Project is not empty.

Hope this helps.

Re: Creating Two Models in One Form

Great tutorial!

How easy would it be to extend it so you could have more than one task as well?