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
end

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

def 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)

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Variable Number of Models in One Form

This is a great tutorial - well written and easy to follow. It was also just what I needed. Thanks ryan.

Re: Creating Variable Number of Models in One Form

I have problem

http://pastie.caboo.se/21015

Strange

Re: Creating Variable Number of Models in One Form

Hi innu, would you mind posting your User model (user.rb)? I think the problem might be in there. I'm guessing the login method might call itself, but I'm not sure.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Variable Number of Models in One Form

http://pastie.caboo.se/21106

I think that its not user.rb error.

If i dont use partial, just use code in the content, then there is no error. Only if render partial, the error will appear.

Re: Creating Variable Number of Models in One Form

I've tried to duplicate the problem, but haven't succeeded (it is working fine for me).

I have also researched the stack trace, and the problem appears to occur when passing the user as a local variable into the partial. It calls "hash" on the user which results in an infinite loop over method_missing. I'm wondering if the problem is in the users table (database schema). Perhaps there is a column name which is overriding a method in active_record? I'm not sure.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Variable Number of Models in One Form

I have really stupid feeling.

Today, I turned my pc on and its all working. Maybe it just needed server restart, but i dont know why. Anyway, thanks for help.

Edit:

Btw.

<% fields_for "user[#{index}]", user do |f| %>
...

generates fields like:
<input id="user[0]_login" name="user[0][login]" size="14" type="text" />

Almous good, except "character "[" is not allowed in the value of attribute "id"." ( XHTML 1.0 Transitional ). Should i change all fields :id-s by hand or ?

Last edited by innu (2006-11-04 05:39:05)

Re: Creating Variable Number of Models in One Form

innu wrote:

Almous good, except "character "[" is not allowed in the value of attribute "id"." ( XHTML 1.0 Transitional ). Should i change all fields :id-s by hand or ?

Yeah, if you need to reference it I would do it by hand. It is just important that we have [0] in the name so we can loop through the fields in the resulting action.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Variable Number of Models in One Form

Man Ryan--you are posting the exact tutorials that I need! Thank you so much dude.

Re: Creating Variable Number of Models in One Form

Alright--as last tutorial I had trouble passing a third model within the whole scheme of things.

Heres the error:

"NoMethodError in MemoriesController#create

You have a nil object when you didn't expect it!
The error occured while evaluating nil.each_value

Parameters: {"memory"=>{"memory_date(2i)"=>"11", "memory_date(3i)"=>"8", "title"=>"weeee", "memory_date(1i)"=>"2006"}, "commit"=>"Create", "conversation_1"=>{"message"=>"test 2", "author_id"=>"2"}, "conversation_"=>{"message"=>"test 1", "author_id"=>"1"}}

Show session dump
---
flash: !map:ActionController::Flash::FlashHash {}"


and my code:

 # memories.controller.rb
   def create
     @memory = Memory.new(params[:memory])
     params[:conversations].each_value { |conversation| @memory.conversations.build(conversation) }
     if @memory.save
       redirect_to :action => 'index'
     else
       render :action => 'new'
     end
   end

 #new.rhtml
<h1>New memory</h1>

<%= start_form_tag :action => 'create' %>

<p><label for="memory_title">Memory Title</label><br/>
<%= text_field 'memory', 'title'  %></p>

<p><label for="memory_memory_date">Memory Date</label><br/>
<%= date_select 'memory', 'memory_date'  %></p>

<div id="conversations">
    <% @memory.conversations.each_with_index do |conversation, authors, index| %>
        <%= render :partial => 'conversation_fields', :locals => { :conversation => conversation, :authors => authors, :index => index } %>
    <% end %>
</div>

<%= render :partial => 'add_conversation_link', :locals => { :index => @memory.conversations.size } %>



  <%= submit_tag "Create" %>
<%= end_form_tag %>

<%= link_to 'Back', :action => 'list' %>


 #_add_conversation_link.rhtml
<div id="add_conversation_link">
<%= link_to_remote 'Add Another Conversation', :url => { :action => 'add_conversation', :index => index } %>
</div>

 #add_conversation.rjs
page.insert_html :bottom, :conversations, :partial => 'conversation_fields',
                  :locals => { :conversation => @conversation, :index => params[:index] }
 
page.replace :add_conversation_link, :partial => 'add_conversation_link', :locals => { :index => (params[:index].to_i + 1) }

 #_conversation_fields.rhtml
<div id="conversation_<%= index %>">
    <% fields_for "conversation_#{index}", conversation do |f| %>
     <%= f.collection_select(:author_id, @authors, :id, :name) %>
     <%= f.text_field :message %>  <%= link_to_remote 'remove', :url => { :action => 'remove_conversation', :index => index } %>
    <% end %>
</div>

Ryan I completely appreciate your help on this one!

Re: Creating Variable Number of Models in One Form

On the fields_for conversation line, try this:

<% fields_for "conversations[#{index}]", conversation do |f| %>

This should send the conversation parameters as a hash so you can loop through them in the controller.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Variable Number of Models in One Form

ryanb wrote:

<% fields_for "conversations[#{index}]", conversation do |f| %>

This should send the conversation parameters as a hash so you can loop through them in the controller.

Kk. Tried that--but now I can't get:

" NoMethodError in Memories#new

Showing app/views/memories/_conversation_fields.rhtml where line #3 raised:

You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occured while evaluating nil.id_before_type_cast

Extracted source (around line #3):

1: <div id="conversation_<%= index %>">
2:     <% fields_for "conversations[#{index}]", conversation do |f| %>
3:      <%= f.collection_select(:author_id, @authors, :id, :name) %>
4:      <%= f.text_field :message %>  <%= link_to_remote 'remove', :url => { :action => 'remove_conversation', :index => index } %>
5:     <% end %>
6: </div>"

Re: Creating Variable Number of Models in One Form

This happens when the value of the "index" variable is nil. The problem is it isn't getting set in the loop in new.rhtml:

  <% @memory.conversations.each_with_index do |conversation, authors, index| %>
    <%= render :partial => 'conversation_fields', :locals => { :conversation => conversation, :authors => authors, :index => index } %>
  <% end %>

This should be:

<% @memory.conversations.each_with_index do |conversation, index| %>
  <%= render :partial => 'conversation_fields', :locals => { :conversation => conversation, :index => index } %>
<% end %>

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Variable Number of Models in One Form

Ryan--thanks for the reply i'll try it out later.

The reason i was passing the :authors variable is because I thought it was necessary to edit the fields and all that, since i'm trying to pass another model in this equation.

Re: Creating Variable Number of Models in One Form

darrenemo wrote:

The reason i was passing the :authors variable is because I thought it was necessary to edit the fields and all that, since i'm trying to pass another model in this equation.

Here you are referencing the authors as an instance variable (@authors) so it's not necessary to pass it as a local variable. However, you will need to set up this @authors instance variable in every action that renders this partial (this including the add_conversation action).

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Variable Number of Models in One Form

ryanb wrote:

...However, you will need to set up this @authors instance variable in every action that renders this partial (this including the add_conversation action).

How would I do this in the add_conversation action?

@authors = Author.find_all  (?)

Or would it be different since the authors elements are being passed through all the methods?

Re: Creating Variable Number of Models in One Form

Yeah, however you are doing it in the other action. Probably find(:all)

def add_conversation
  @conversation = Conversation.new
  @authors = Author.find(:all)
end

You should be good to go then.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Variable Number of Models in One Form

Alright i was successfully able to get the authors saved from the new.rhtml template.

Now my next question is how to do I successfully add a new conversation, under the edit.rhtml template?

I added the snippet from new.rhtml to add a new convo (in edit.rhtml):

<div id="conversations">
    <% @memory.conversations.each_with_index do |conversation, index| %>
        <%= render :partial => 'conversation_fields', :locals => { :conversation => conversation, :index => index } %>
    <% end %>
</div>

<%= render :partial => 'add_conversation_link', :locals => { :index => @memory.conversations.size } %>


But I dont know how to combine the two methods (create and update) so that it resaves the existing convos, but adds a new one. 

When I add a

#memories.controller.rb 
params[:conversations].each_value { |conversation| @memory.conversations.build(conversation) }

to my update function, it successfully saves the new convos (from the edit.rhtml) but also resaves the previous ones as new conversations, duplicating them.

How can I combine the two methods?

Also, when I'm editing a memory, the rjs remove snippet removes the field on my page, but it doesn't save those changes either.  here's my code:

   def update
     @memory = Memory.find(params[:id])
     @memory.attributes = params[:memory]
     @memory.conversations.each { |t| t.attributes = params["conversation_#{t.id}"] }
     if @memory.valid? && @memory.conversations.all?(&:valid?)
       @memory.save!
       @memory.conversations.each(&:save!)
       redirect_to :action => 'show', :id => @memory
     else
       render :action => 'edit'
     end
   end

   def create
     @memory = Memory.new(params[:memory])
     params[:conversations].each_value { |conversation| @memory.conversations.build(conversation) }
     if @memory.save
       redirect_to :action => 'index'
     else
       render :action => 'new'
     end
   end


#edit.rhtml
<div id="conversations">
    <% @memory.conversations.each_with_index do |conversation, index| %>
        <%= render :partial => 'conversation_fields', :locals => { :conversation => conversation, :index => index } %>
    <% end %>
</div>

<%= render :partial => 'add_conversation_link', :locals => { :index => @memory.conversations.size } %>


And my third question is, how can I successfully delete the memory AND all the attributes with it?  I've toyed around with adding variables (@memory.conversations.each.delete) and such to no avail.  The memory successfully gets deleted, but the accompanying conversations don't

  def destroy
    Memory.find(params[:id]).destroy
    redirect_to :action => 'list'
  end

#list.rhtml snippet
...<td><%= link_to 'Destroy', { :action => 'destroy', :id => memory }, :confirm => 'Are you sure?', :post => true %></td>...

Thanks for your help thus far--Ryan youve been invaluable in this project!

Re: Creating Variable Number of Models in One Form

darrenemo wrote:

And my third question is, how can I successfully delete the memory AND all the attributes with it?  I've toyed around with adding variables (@memory.conversations.each.delete) and such to no avail.  The memory successfully gets deleted, but the accompanying conversations don't

Look into the :dependent parameter in the has_many association.

has_many :conversations, :dependent => :destroy

As for your other questions, I'm afraid you'll have to wait for another tutorial as this can get rather tricky.

Railscasts - Free Ruby on Rails Screencasts

Re: Creating Variable Number of Models in One Form

ryanb wrote:

As for your other questions, I'm afraid you'll have to wait for another tutorial as this can get rather tricky.

Alright--thanks man for your help!