Rails Forum
Rails Work - the best place to post and find great Ruby on Rails jobs.
Username
Password

You are not logged in.

New Posts in this thread
  • Index
  •  » Tutorials
  •  » Creating Variable Number of Models in One Form

#1 2006-11-02 00:53:05

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

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.


Code :  ruby - fold - unfold
  1. # in projects_controller.rb
  2. def new
  3.   @project = Project.new
  4.   @project.tasks.build # creates just one task
  5. end
  6.  
  7. # in projects/new.rhtml
  8. <% @project.tasks.each_with_index do |task, index| %>
  9.   <% fields_for "tasks[#{index}]", task do |f| %>
  10.     <p><%= f.text_field :name %></p>
  11.   <% end %>
  12. <% 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:


Code :  ruby - fold - unfold
  1. # in projects/new.rhtml
  2. <div id="tasks">
  3. <% @project.tasks.each_with_index do |task, index| %>
  4.   <%= render :partial => 'task_fields', :locals => { :task => task, :index => index } %>
  5. <% end %>
  6. </div>
  7.  
  8. # in projects/_task_fields.rhtml
  9. <div id="task_<%= index %>">
  10. <% fields_for "tasks[#{index}]", task do |f| %>
  11.   <p><%= f.text_field :name %></p>
  12. <% end %>
  13. </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)


Code :  ruby - fold - unfold
  1. # in projects/new.rhtml
  2. <%= 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.


Code :  ruby - fold - unfold
  1. # in projects_controller.rb
  2. def add_task
  3.   @task = Task.new
  4. end
  5.  
  6. # in projects/add_task.rjs
  7. 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:


Code :  ruby - fold - unfold
  1. # in projects/new.rhtml
  2. <%= link_to_remote 'Add Another Task', :url => { :action => 'add_task', :index => @project.tasks.size } %>
  3.  
  4. # in projects/add_task.rjs
  5. page.insert_html :bottom, :tasks, :partial => 'task_fields',
  6.                  :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.


Code :  ruby - fold - unfold
  1. # in projects/new.rhtml
  2. <%= render :partial => 'add_task_link', :locals => { :index => @project.tasks.size } %>
  3.  
  4. # in projects/_add_task_link.rhtml
  5. <div id="add_task_link">
  6.   <%= link_to_remote 'Add Another Task', :url => { :action => 'add_task', :index => index } %>
  7. </div>
  8.  
  9. # in projects/add_task.rjs
  10. 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:


Code :  ruby - fold - unfold
  1. # in projects/_task_fields.rhtml
  2. <%= link_to_remote 'remove', :url => { :action => 'remove_task', :index => index } %>
  3.  
  4. # remove_task.rjs
  5. 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:


Code :  ruby - fold - unfold
  1. # projects_controller.rb
  2. def new
  3.   @project = Project.new
  4.   @project.tasks.build
  5. end
  6.  
  7. def create
  8.   @project = Project.new(params[:project])
  9.   params[:tasks].each_value { |task| @project.tasks.build(task) }
  10.   if @project.save
  11.     redirect_to :action => 'index'
  12.   else
  13.     render :action => 'new'
  14.   end
  15. end
  16.  
  17. def add_task
  18.   @task = Task.new
  19. end
  20.  
  21. # projects/new.rhtml
  22. <% form_for :project, :url => { :action => 'create' } do |f| %>
  23.   <p>Name: <%= f.text_field :name %></p>
  24.   <h2>Tasks</h2>
  25.   <div id="tasks">
  26.   <% @project.tasks.each_with_index do |task, index| %>
  27.     <%= render :partial => 'task_fields', :locals => { :task => task, :index => index } %>
  28.   <% end %>
  29.   </div>
  30.   <%= render :partial => 'add_task_link', :locals => { :index => @project.tasks.size } %>
  31.   <p><%= submit_tag 'Create Project' %></p>
  32. <% end %>
  33.  
  34. # projects/_task_fields.rhtml
  35. <div id="task_<%= index %>">
  36. <% fields_for "tasks[#{index}]", task do |f| %>
  37.   <p>
  38.     <%= f.text_field :name %>
  39.     <%= link_to_remote 'remove', :url => { :action => 'remove_task', :index => index } %>
  40.   </p>
  41. <% end %>
  42. </div>
  43.  
  44. # projects/_add_task_link.rhtml
  45. <div id="add_task_link">
  46.   <%= link_to_remote 'Add Another Task', :url => { :action => 'add_task', :index => index } %>
  47. </div>
  48.  
  49. # projects/add_task.rjs
  50. page.insert_html :bottom, :tasks, :partial => 'task_fields',
  51.                  :locals => { :task => @task, :index => params[:index] }
  52.  
  53. page.replace :add_task_link, :partial => 'add_task_link', :locals => { :index => (params[:index].to_i + 1) }
  54.  
  55. # projects/remove_task.rjs
  56. page["task_#{params[:index]}"].remove 

Last edited by ryanb (2006-11-30 19:25:56)


Railscasts - Free Ruby on Rails Screencasts

Offline

 

#2 2006-11-02 17:50:52

cathyb
Passenger
From: Stockport, UK
Registered: 2006-10-30
Posts: 25
Website

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.

Offline

 

#3 2006-11-03 12:57:48

innu
Coach Class
Registered: 2006-09-27
Posts: 64

Re: Creating Variable Number of Models in One Form

I have problem

http://pastie.caboo.se/21015

Strange

Offline

 

#4 2006-11-03 16:19:53

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

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

Offline

 

#5 2006-11-03 23:47:14

innu
Coach Class
Registered: 2006-09-27
Posts: 64

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.

Offline

 

#6 2006-11-04 00:32:15

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

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

Offline

 

#7 2006-11-04 09:23:00

innu
Coach Class
Registered: 2006-09-27
Posts: 64

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.

Code :   - fold - unfold
  1. <% fields_for "user[#{index}]", user do |f| %>
  2. ...
generates fields like:

Code :   - fold - unfold
  1. <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 09:39:05)

Offline

 

#8 2006-11-04 16:34:30

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

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

Offline

 

#9 2006-11-06 19:06:39

darrenemo
Ticketholder
From: Idaho / California
Registered: 2006-11-01
Posts: 20

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.

Offline

 

#10 2006-11-08 08:48:02

darrenemo
Ticketholder
From: Idaho / California
Registered: 2006-11-01
Posts: 20

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:

Code :   - fold - unfold
  1. # memories.controller.rb
  2.    def create
  3.      @memory = Memory.new(params[:memory])
  4.      params[:conversations].each_value { |conversation| @memory.conversations.build(conversation) }
  5.      if @memory.save
  6.        redirect_to :action => 'index'
  7.      else
  8.        render :action => 'new'
  9.      end
  10.    end

Code :   - fold - unfold
  1. #new.rhtml
  2. <h1>New memory</h1>
  3.  
  4. <%= start_form_tag :action => 'create' %>
  5.  
  6. <p><label for="memory_title">Memory Title</label><br/>
  7. <%= text_field 'memory', 'title'  %></p>
  8.  
  9. <p><label for="memory_memory_date">Memory Date</label><br/>
  10. <%= date_select 'memory', 'memory_date'  %></p>
  11.  
  12. <div id="conversations">
  13.   <% @memory.conversations.each_with_index do |conversation, authors, index| %>
  14.     <%= render :partial => 'conversation_fields', :locals => { :conversation => conversation, :authors => authors, :index => index } %>
  15.   <% end %>
  16. </div>
  17.  
  18. <%= render :partial => 'add_conversation_link', :locals => { :index => @memory.conversations.size } %>
  19.  
  20.  
  21.  
  22.   <%= submit_tag "Create" %>
  23. <%= end_form_tag %>
  24.  
  25. <%= link_to 'Back', :action => 'list' %>

Code :   - fold - unfold
  1. #_add_conversation_link.rhtml
  2. <div id="add_conversation_link">
  3. <%= link_to_remote 'Add Another Conversation', :url => { :action => 'add_conversation', :index => index } %>
  4. </div>

Code :   - fold - unfold
  1. #add_conversation.rjs
  2. page.insert_html :bottom, :conversations, :partial => 'conversation_fields',
  3.                   :locals => { :conversation => @conversation, :index => params[:index] }
  4.   
  5. page.replace :add_conversation_link, :partial => 'add_conversation_link', :locals => { :index => (params[:index].to_i + 1) }

Code :   - fold - unfold
  1. #_conversation_fields.rhtml
  2. <div id="conversation_<%= index %>">
  3.   <% fields_for "conversation_#{index}", conversation do |f| %>
  4.    <%= f.collection_select(:author_id, @authors, :id, :name) %>
  5.    <%= f.text_field :message %>  <%= link_to_remote 'remove', :url => { :action => 'remove_conversation', :index => index } %>
  6.   <% end %>
  7. </div>
Ryan I completely appreciate your help on this one!

Offline

 

#11 2006-11-08 15:31:38

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

Re: Creating Variable Number of Models in One Form

On the fields_for conversation line, try this:


Code :  ruby - fold - unfold
  1. <% 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

Offline

 

#12 2006-11-08 16:20:47

darrenemo
Ticketholder
From: Idaho / California
Registered: 2006-11-01
Posts: 20

Re: Creating Variable Number of Models in One Form

ryanb wrote:


Code :  ruby - fold - unfold
  1. <% 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>"

Offline

 

#13 2006-11-08 16:46:38

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

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:


Code :  ruby - fold - unfold
  1.   <% @memory.conversations.each_with_index do |conversation, authors, index| %>
  2.     <%= render :partial => 'conversation_fields', :locals => { :conversation => conversation, :authors => authors, :index => index } %>
  3.   <% end %>
This should be:


Code :  ruby - fold - unfold
  1. <% @memory.conversations.each_with_index do |conversation, index| %>
  2.   <%= render :partial => 'conversation_fields', :locals => { :conversation => conversation, :index => index } %>
  3. <% end %>


Railscasts - Free Ruby on Rails Screencasts

Offline

 

#14 2006-11-08 22:14:17

darrenemo
Ticketholder
From: Idaho / California
Registered: 2006-11-01
Posts: 20

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.

Offline

 

#15 2006-11-08 22:37:52

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

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

Offline

 

#16 2006-11-09 01:09:12

darrenemo
Ticketholder
From: Idaho / California
Registered: 2006-11-01
Posts: 20

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?

Offline

 

#17 2006-11-09 05:37:22

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

Re: Creating Variable Number of Models in One Form

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


Code :  ruby - fold - unfold
  1. def add_conversation
  2.   @conversation = Conversation.new
  3.   @authors = Author.find(:all)
  4. end 
You should be good to go then.


Railscasts - Free Ruby on Rails Screencasts

Offline

 

#18 2006-11-09 08:37:14

darrenemo
Ticketholder
From: Idaho / California
Registered: 2006-11-01
Posts: 20

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

Code :   - fold - unfold
  1. <div id="conversations">
  2.   <% @memory.conversations.each_with_index do |conversation, index| %>
  3.     <%= render :partial => 'conversation_fields', :locals => { :conversation => conversation, :index => index } %>
  4.   <% end %>
  5. </div>
  6.  
  7. <%= 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

Code :   - fold - unfold
  1. #memories.controller.rb
  2. 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:

Code :   - fold - unfold
  1.    def update
  2.      @memory = Memory.find(params[:id])
  3.      @memory.attributes = params[:memory]
  4.      @memory.conversations.each { |t| t.attributes = params["conversation_#{t.id}"] }
  5.      if @memory.valid? && @memory.conversations.all?(&:valid?)
  6.        @memory.save!
  7.        @memory.conversations.each(&:save!)
  8.        redirect_to :action => 'show', :id => @memory
  9.      else
  10.        render :action => 'edit'
  11.      end
  12.    end
  13.  
  14.    def create
  15.      @memory = Memory.new(params[:memory])
  16.      params[:conversations].each_value { |conversation| @memory.conversations.build(conversation) }
  17.      if @memory.save
  18.        redirect_to :action => 'index'
  19.      else
  20.        render :action => 'new'
  21.      end
  22.    end

Code :   - fold - unfold
  1. #edit.rhtml
  2. <div id="conversations">
  3.   <% @memory.conversations.each_with_index do |conversation, index| %>
  4.     <%= render :partial => 'conversation_fields', :locals => { :conversation => conversation, :index => index } %>
  5.   <% end %>
  6. </div>
  7.  
  8. <%= 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


Code :   - fold - unfold
  1.   def destroy
  2.     Memory.find(params[:id]).destroy
  3.     redirect_to :action => 'list'
  4.   end

Code :   - fold - unfold
  1. #list.rhtml snippet
  2. ...<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!

Offline

 

#19 2006-11-09 17:11:56

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

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.


Code :  ruby - fold - unfold
  1. 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

Offline

 

#20 2006-11-09 21:35:05

darrenemo
Ticketholder
From: Idaho / California
Registered: 2006-11-01
Posts: 20

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!

Offline

 
  • Index
  •  » Tutorials
  •  » Creating Variable Number of Models in One Form

Board footer

Powered by PunBB
© Copyright 2002–2005 Rickard Andersson