Topic: Editing Multiple Models in One Form
In the previous article I showed you how to create a project and a task in the same form. In this article I will show you how to edit a project and all of its tasks in one form.
Let's jump right in. The edit action in the controller is simple enough:
# in projects_controller.rb
def edit
@project = Project.find(params[:id])
end
The real complexity is in the view. The form needs to loop through @project.tasks and display the fields for each. This will be a problem because each field must have a unique name. How do we accomplish this? We can just append the id of the task to the field name.
This brings us to another problem: appending the id of the model makes it difficult to work with form helper methods because they require an instance variable to exist with the same name as the field model. We don't want to create @task_1, @task_2, etc. instance variables, so that's not going to work for us.
Have you ever heard of the form_for method? It allows you to specify the model when you set up the form so you don't have to specify it for each helper method. The real reason we want to use this is it allows us to specify the name and the instance of the model so we don't have to worry about making the names match.
This brings us to yet another problem, we can't use form_for because that creates a <form> element and we want this all to exist in one form. It just so happens that form_for has a cousin method: fields_for. This works just like form_for but doesn't generate form element. That's exactly what we need!
Phew, I'm done talking. Time to show some code:
# in projects/edit.rhtml
<h1>Edit Project</h1><%= error_messages_for :project %>
<%= start_form_tag :action => 'update', :id => params[:id] %>
<p>
Project Name:
<%= text_field :project, :name %>
</p><h2>Tasks</h2>
<% for @task in @project.tasks %>
<%= error_messages_for :task %>
<% fields_for "task[]" do |f| %>
<p><%= f.text_field :name %></p>
<% end %>
<% end %><p><%= submit_tag 'Update' %></p>
<%= end_form_tag %>
Rails is smart enough to automatically place the id inside the task[] hash for each field, so the resulting html output will look like this for the name field of one task:
<p><input id="task_1_name" name="task[1][name]" size="30" type="text" value="thing" /></p>
You probably notice I snuck the error_messages_for :task in that task loop. This is good enough for development, but is kind of ugly. Before moving onto production I recommend cleaning this up and manually looping through the error messages and displaying them however you like.
Now, on to the update action. Let's summarize what this action must accomplish: update the project and all task attributes, validate the project and all tasks, and save the project and all tasks if valid. Sounds like things can get pretty complex, so let's take it one step at a time.
1. Update the project attributes
This is easy enough, just grab the project and set the attributes. We don't want to use the update_attributes method here because we don't want to save the project (the tasks might be invalid).
@project = Project.find(params[:id])
@project.attributes = params[:project]
2. Update each task attributes
We need to loop through each task and update its attributes, just like we did for the project itself.
@project.tasks.each { |t| t.attributes = params[:task][t.id.to_s] }3. Validate the project and all tasks
We can use the all? enumerable method to help us out in this validation (along with the Symbol#to_proc hack to make it a little more concise).
if @project.valid? && @project.tasks.all?(&:valid?)
4. Save the project and all tasks
Just loop through the tasks and call save! on each one.
@project.save!
@project.tasks.each(&:save!)
The end result looks like this:
# in projects_controller.rb
def update
@project = Project.find(params[:id])
@project.attributes = params[:project]
@project.tasks.each { |t| t.attributes = params[:task][t.id.to_s] }
if @project.valid? && @project.tasks.all?(&:valid?)
@project.save!
@project.tasks.each(&:save!)
redirect_to :action => 'show', :id => @project
else
render :action => 'edit'
end
end
That's a large chunk of code to take in, but if you just look at each line individually, it's not too bad. If someone knows a better way to handle this, please reply to this thread as I would love to know.
That finishes this tutorial. Next time I will discuss creating multiple models in one form (more than two).
Last edited by ryanb (2006-11-08 12:36:45)