Topic: Creating Variable Number of Models in One Form
In the previous article I showed you how to create a project and five tasks all in one form. Here I will show you how to add/remove tasks in that same form using JavaScript and RJS.
Let's start off with the code we created in the last article. But, we'll just start with one task instead of five since they can be added dynamically.
# in projects_controller.rb
def new
@project = Project.new
@project.tasks.build # creates just one task
end# 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 %>
In order to add tasks dynamically using RJS, we need to move the task fields into a partial. Let's also include some divs so they can easily be referenced through RJS:
# in projects/new.rhtml
<div id="tasks">
<% @project.tasks.each_with_index do |task, index| %>
<%= render :partial => 'task_fields', :locals => { :task => task, :index => index } %>
<% end %>
</div># in projects/_task_fields.rhtml
<div id="task_<%= index %>">
<% fields_for "tasks[#{index}]", task do |f| %>
<p><%= f.text_field :name %></p>
<% end %>
</div>
Perfect. Notice we are passing the index and task to the partial as local variables so they can be referenced there without problem.
Next we need to create a link to add tasks dynamically through RJS. (This should go after the "tasks" div)
# in projects/new.rhtml
<%= link_to_remote 'Add Another Task', :url => { :action => 'add_task' } %>
We are using AJAX here, so don't forget to include the appropriate javascript files in the <head> element.
Clicking the link won't do anything yet since we haven't defined the add_task method in the controller. We need that action to create a new task and insert the fields into the page using RJS.
# in projects_controller.rb
def add_task
@task = Task.new
end# in projects/add_task.rjs
page.insert_html :bottom, :tasks, :partial => 'task_fields', :locals => { :task => @task }
Uh oh, we have a problem. The task_fields partial wants an "index" local variable, but we aren't passing it one. In fact, there's no way for us to determine what the index should be in this method because it is an entirely new request. But, have no fear! The next index can be determined when generating the original link. We can pass it as a parameter to this action:
# in projects/new.rhtml
<%= link_to_remote 'Add Another Task', :url => { :action => 'add_task', :index => @project.tasks.size } %># in projects/add_task.rjs
page.insert_html :bottom, :tasks, :partial => 'task_fields',
:locals => { :task => @task, :index => params[:index] }
Yay, that works! .... almost anyway. The first task we add has the correct index, but then every task we add after that has the same index. We need it to increment the index every time a task is added. In other words, we need to update the link at the same time we add the fields. This actually isn't too difficult. To do this we need to move the link into a partial and update it the same time we update the other field.
# in projects/new.rhtml
<%= render :partial => 'add_task_link', :locals => { :index => @project.tasks.size } %># in projects/_add_task_link.rhtml
<div id="add_task_link">
<%= link_to_remote 'Add Another Task', :url => { :action => 'add_task', :index => index } %>
</div># in projects/add_task.rjs
page.replace :add_task_link, :partial => 'add_task_link', :locals => { :index => (params[:index].to_i + 1) }
Adding tasks should now properly increment the index so the field names are unique. The last thing we need to do is make a way to remove the tasks. This can be done by creating a "remove" link next to each task linking to an action which deletes the div from the view. Here's the code:
# in projects/_task_fields.rhtml
<%= link_to_remote 'remove', :url => { :action => 'remove_task', :index => index } %># remove_task.rjs
page["task_#{params[:index]}"].remove
It's that simple. Now tasks can be removed and added dynamically and they will always have a unique field name. We don't even have to change the create action from the last article. Here's the final code:
# projects_controller.rb
def new
@project = Project.new
@project.tasks.build
enddef 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
enddef add_task
@task = Task.new
end# projects/new.rhtml
<% form_for :project, :url => { :action => 'create' } do |f| %>
<p>Name: <%= f.text_field :name %></p>
<h2>Tasks</h2>
<div id="tasks">
<% @project.tasks.each_with_index do |task, index| %>
<%= render :partial => 'task_fields', :locals => { :task => task, :index => index } %>
<% end %>
</div>
<%= render :partial => 'add_task_link', :locals => { :index => @project.tasks.size } %>
<p><%= submit_tag 'Create Project' %></p>
<% end %># projects/_task_fields.rhtml
<div id="task_<%= index %>">
<% fields_for "tasks[#{index}]", task do |f| %>
<p>
<%= f.text_field :name %>
<%= link_to_remote 'remove', :url => { :action => 'remove_task', :index => index } %>
</p>
<% end %>
</div># projects/_add_task_link.rhtml
<div id="add_task_link">
<%= link_to_remote 'Add Another Task', :url => { :action => 'add_task', :index => index } %>
</div># projects/add_task.rjs
page.insert_html :bottom, :tasks, :partial => 'task_fields',
:locals => { :task => @task, :index => params[:index] }page.replace :add_task_link, :partial => 'add_task_link', :locals => { :index => (params[:index].to_i + 1) }
# projects/remove_task.rjs
page["task_#{params[:index]}"].remove
Last edited by ryanb (2006-11-30 15:25:56)