#75
Oct 15, 2007

Complex Forms Part 3

In this third and final episode on complex forms I will show you how to edit a project and multiple tasks all in one form. This includes removing and adding tasks dynamically as well. See the show notes for updated code.
Download (25.7 MB, 16:11)
alternative download for iPod & Apple TV (20.5 MB, 16:11)

Resources

Update (3/29/2008)

The code for this screencast isn’t the greatest, it requires the :index option for the fields and does not work well in Rails 2. Instead I recommend going with the approach I show in Advanced Rails Recipes. Here’s the code (posted by permission). Check out the recipe for details on how it works.

If you want, you can also view the original code.

<!-- projects/edit.rhtml -->
<% form_for :project, :url => project_path(@project), :html => { :method => 'put' } do |f| %>
  <%= render :partial => 'fields', :locals => { :f => f } %>
  <p><%= submit_tag "Update Project" %></p>
<% end %>

<!-- projects/new.rhtml -->
<% form_for :project, :url => projects_path do |f| %>
  <%= render :partial => 'fields', :locals => { :f => f } %>
  <p><%= submit_tag "Create Project" %></p>
<% end %>

<!-- projects/_fields.rhtml -->
<p>
  Name: <%= f.text_field :name %>
</p>
<div id="tasks">
  <%= render :partial => 'task', :collection => @project.tasks %>
</div>
<p><%= add_task_link "Add a task" %></p>


<!-- projects/_task.rhtml -->
<div class="task">
  <% fields_for_task(task) do |task_form| %>
  <p>
    Task: <%= task_form.text_field :name %>
    <%= link_to_function "remove", "$(this).up('.task').remove()" %>
  </p>
  <% end %>
</div>
# models/project.rb
class Project < ActiveRecord::Base
  has_many :tasks, :dependent => :destroy

  validates_presence_of :name
  validates_associated :tasks
  
  after_update :save_tasks

  def new_task_attributes=(task_attributes)
    task_attributes.each do |attributes|
      tasks.build(attributes)
    end
  end
  
  def existing_task_attributes=(task_attributes)
    tasks.reject(&:new_record?).each do |task|
      attributes = task_attributes[task.id.to_s]
      if attributes
        task.attributes = attributes
      else
        tasks.delete(task)
      end
    end
  end
  
  def save_tasks
    tasks.each do |task|
      task.save(false)
    end
  end
end


# models/task.rb
class Task < ActiveRecord::Base
  belongs_to :project
  validates_presence_of :name
end


# projects_controller.rb
def new
  @project = Project.new
  @project.tasks.build
end

def create
  @project = Project.new(params[:project])
  if @project.save
    flash[:notice] = "Successfully created project and tasks."
    redirect_to projects_path
  else
    render :action => 'new'
  end
end

def edit
  @project = Project.find(params[:id])
end

def update
  params[:project][:existing_task_attributes] ||= {}
  
  @project = Project.find(params[:id])
  if @project.update_attributes(params[:project])
    flash[:notice] = "Successfully updated project and tasks."
    redirect_to project_path(@project)
  else
    render :action => 'edit'
  end
end


# projects_helper.rb
def fields_for_task(task, &block)
  prefix = task.new_record? ? 'new' : 'existing'
  fields_for("project[#{prefix}_task_attributes][]", task, &block)
end

def add_task_link(name) 
  link_to_function name do |page| 
    page.insert_html :bottom, :tasks, :partial => 'task', :object => Task.new 
  end 
end 

Validation

Displaying the validation errors can be tricky depending on your needs. It may be as simple as displaying the project validations:

<!-- projects/_fields.rhtml -->
<%= error_messages_for :project %>

This will mention if there are task validation errors, however it will not state exactly what the errors are because these are stored in each task model.

If you need to display the task error messages. One way is to do this:

<!-- projects/_task.rhtml -->
<% @task = task %>
<%= error_messages_for :task %>

The error_messages_for method requires an instance variable with the same name, so that is why it needs to be set right there. This is an ugly work around. If you want to do it in a cleaner way or handle the error messages differently then I recommend creating your own validation helper method instead. I may go into more details in a future episode.

If you want to share your validation troubles feel free to add a comment!

RSS Feed for Episode Comments 161 comments

1. Jorge Calás Oct 15, 2007 at 01:54

Great! As always.

Thanks very much for sharing all this, helping us to be better programmers.

Watching your screencast has become my every monday's firt task ;)


2. Ilya Sabanin Oct 15, 2007 at 04:27

I believe that you should delete :id from parameters before model going to save or you will get error like this: "Can't mass-assign these protected attributes: id".

Here is the code of method in the Project model that will fix that issue:

def task_attributes=(task_attributes)
    task_attributes.each do |attributes|
      unless id = attributes.delete(:id)
        tasks.build(attributes)
      else
        task = tasks.detect { |t| t.id == id.to_i }
        task.attributes = attributes
      end
    end
  end


3. pimpmaster Oct 15, 2007 at 04:36

That was probably the most intense Railscast yet. Unbelievable how much ground you covered.

Excellent stuff as always

+1 vote for a future episode on associated validations.

:)


4. Andrew Turner Oct 15, 2007 at 06:21

Have you tried out this technique in Rails 2.0 preview? The task_attributes= method doesn't seem to be getting called on @project.update_attributes.

However, the Task#update method is still getting called, so I wonder if this was another change in Rails 2.0, eliminating the need for the after_update filter on Project.


5. George Oct 15, 2007 at 07:56

Hi Ryan,

Nice screencast! :)

One thing came to mind, though: instead of sending destroy flags from the client side, wouldn't it be easier to set the flag in Project#task_attributes= for any record for which there's no attributes passed in?


6. Ryan Bates Oct 15, 2007 at 08:22

@Ilya & Andrew, I have yet to test this extensively in edge rails and 2.0 preview. I'll do that and try to post the fixes in the show notes.

@George, that's an interesting idea. I hadn't thought of that! I'm wondering if it will be anymore difficult to carry the state across when validation fails. I'll have to play around with it. Thanks!


7. TheCompWiz Oct 15, 2007 at 10:05

Just out of curiosity... is there any reason you chose to use "task.save" as opposed to "task.update_attributes"?


8. Ted Oct 15, 2007 at 10:51

Hey Ryan, another great episode.

I hope somebody convinces you do to a part 4 covering the error messages :-)

Just a matter of style preference, shouldn't save_tasks in Project be marked as private? Perhaps it's just my old-school tendencies but I try to make everything private by default and only expose what needs to be exposed. I understand if you just didn't bother in order to keep the episode on topic.


9. Taalas Oct 15, 2007 at 12:25

Hey Ryan,
thanks for another curious ingenious screencast. Before I was doing that Javascript stuff with rendering a partial into a string and then inserting this with js. But your method is more rubyish.

But one thing is getting me a headache. You are using :index => nil in the form helpers for text_field. I tried to use that with select but it does not work. The index is rendered anyway. Do you have any clue?


10. Wes Oct 15, 2007 at 13:10

Taalas,

You have to pass an empty hash as an option before the :index => nil

Something like this will work:
f.select :field, [ "option 1", "option 2" ], {}, :index => nil

Figured this out from http://dev.rubyonrails.org/ticket/589


11. Taalas Oct 15, 2007 at 13:25

Hi Wes,

Thanks for that. I was stuck often because of that "empty hash" thing...
I don't know why I always tap into that trap.


12. Etandrib Oct 15, 2007 at 14:04

JS question:

How do I make the mark_for_destroy function more generic so I can apply it to multiple models (ie. phones, email addresses, etc.)

right now it is the following:
  $(element).up('.phone').hide();

I'm not proficient in JS so any basic help is appreciated. Thanks!


13. Brandon Oct 15, 2007 at 15:25

As I was watching this episode, I notice in your application layout you have a helper called show_title?, related to your "Pretty Page Titles" from episode 30. I was wondering how that particular little snippet works.

I love these screencasts! I've been working on Rails for almost a year now, and you seem to come out with the one that I need just as I need it.


14. Ryan McGeary Oct 15, 2007 at 16:53

Nice episode. I agree that the ":index => nil" snippets are a bit ugly, so I submitted a patch that adds support to pass the index option to the fields_for helper:

http://dev.rubyonrails.org/ticket/9883

Please feel free to "+1" the patch if this patch looks of interest to you.


15. erv2 Oct 15, 2007 at 16:58

Thanks ryan, this has to be one of the longest railscasts.

i would like an episode on error messages. i have two models, one is post, the other is photo. photo is a model using image_column, users can upload 'photo' while they create a post, but it's also important to display the error message of 'photo' while saving, as image_column does validations like format check and size check. i guess quite a lot people are doing it this way, so i really hope there will be an episode on error messages.

once again, thanks ryan for a great episode!!


16. Brian Oct 15, 2007 at 17:43

I love this technique and use it on a few projects I'm working on. A few bits of contention from an accessibility stand point:
1. You should use label tags instead of plain text.
2. One thing that I'd like to see more of is unobtrusive javascript. It's pretty easy to pick up and makes pages more accessible. I think it'd be great for you to push this since you're in a position to reach a lot of people.


17. Michael Oct 15, 2007 at 20:13

Nice tutorial Ryan, I took at stab at pluginizing this technique and have uploaded the plugin and a sample project using it at: http://code.google.com/p/multimodel-forms/

If anyone has any suggestion feel free to email me (my email is on the project home page), or just add it under "Issues". Im currently working on view helpers so try and simplify the views alot.


18. Joe Oct 15, 2007 at 21:25

This is the most awesome yet! I agree with the many others! "Complex Forms Part 4" Single Form Multiple Model Validations! That would be so much help to so many people. Thanks for your amazing work.


19. Joe Oct 16, 2007 at 02:07

I have a question. Let's say my project has many tasks, but it also has many tags and I want the project form to be able to accommodate building two models in reference to a project on the same form. How would one refactor the following *_attributes=(*_attributes) methods to avoid duplication?

  def task_attributes=(task_attributes)
    task_attributes.each do |attributes|
      if attributes[:id].blank?
        tasks.build(attributes)
      else
        task = tasks.detect { |t| t.id == attributes[:id].to_i }
        task.attributes = attributes
      end
    end
  end

  def tag_attributes=(tag_attributes)
    tag_attributes.each do |attributes|
      if attributes[:id].blank?
        tag.build(attributes)
      else
        tag = tags.detect { |t| t.id == attributes[:id].to_i }
        tag.attributes = attributes
      end
    end
  end


20. nicolash Oct 16, 2007 at 03:51

"maybe I will make it [display error messages] a futur episode depending on the demand"
... there is demand ;-)
beside "the demand" there is an other reason to go into this topic: "additional options for presenting model errors -(Changeset #7870)"


21. magistrator Oct 16, 2007 at 06:03

I've got problems with validation of tasks. The has_many relation silently puts up additional validators, which are invoked at the time you try to save the projects model.

The problem is that these hidden validators affect the errors of the project by adding messages like "tasks is invalid" for each invalid task. (In fact, in my case the model names are different, so I'm giving the approximate messages.)

Now, I am seeing two issues with that. First, is there any way to localize the messages produced by has_many? Second, is it anyhow possible to disable adding these messages at all (I want only one message for all failures and I want it customized).

Anyway, thanks for the great job, Ryan!


22. Michael Voigt Oct 16, 2007 at 12:44

Hi Ryan,

very nice screencast. Can you send me the sourcecode of the rails project, i am so sluggardly ;-)

Michael


23. Etandrib Oct 16, 2007 at 13:51

@ Joe

I have the same question. It seems there is a lot of refactor that needs to be done to accommodate multiple sub models.
I posted this question to "refactor my code" to see what the community can come up with. Here is the URL. http://refactormycode.com/codes/87-combine-contact-attributes-in-model


24. Brian Oct 16, 2007 at 14:22

Refactored! Check it out


25. Matthew Lang Oct 17, 2007 at 02:35

Hi Ryan,

Great screencast! Instead of text boxes, should it just be as easy to use multiple select lists?

Thanks,

Matthew


26. piter Oct 17, 2007 at 02:50

There is a bug in railscasts code. When you point your browser to non existent episode (for example http://www.railscasts.com/episodes/100), than your application will crash. I think some <tt>begin/rescue/end</tt> could help ;]


27. Niels Slot Oct 17, 2007 at 03:00

Ryan: You didn't post the code for the javascript mark_for_destroy function.

Here it is for everyone who needs it:

function mark_for_destroy(element) {
    $(element).next('.should_destroy').value = 1
    $(element).up('.task').hide();
}


28. Joe Oct 17, 2007 at 05:03

I have a problem that I can't figure out for the life of me.

I understand the part about
Task: <%= f.text_field :name, :index => nil %>

I must be formatting something wrong or not writing this part write, but how do you set a select list to :index => nil?

Here is my code:
<%= f.select(:phone_type, [ ['',''], ['Home', 'Home'],
['Work', 'Work'], ['Cell', 'Cell'], ['Fax', 'Fax'],
['Other', 'Other'] ]) %>

I have tried :index => nil, tried :selected => nil after the array, but am receiving 'Conflicting types for parameter containers. Expected an instance of Array, but found an instance of Hash.' in my log file. Any help please?


29. Matthew Lang Oct 17, 2007 at 06:36

@Joe,

Any luck there Joe with using select lists? I'm also trying this out but I'm getting the same message as yourself.

Any breakthroughs then mail me.

matthew.langm at gmail.com


30. Matthew Lang Oct 17, 2007 at 07:09

@Joe,

You might want to try add the :index option to the html_options like this

<%= f.collection_select(:phone_type, @phone_types, "id", "name", {}, :index => nil) %>

Hope this helps!


31. Gilles Oct 17, 2007 at 09:50

Great screencast ! But i have one issue, i have read on how to use this technique with select or collection_select, but what about a date_select, the above workaround doesn't work for it ? Any clue to get out of this, anyone plz ?


32. Darryl Oct 17, 2007 at 16:05

I stumbled on the problem with select tags, and using html_options instead of options.

Related to that, checkboxes and radio buttons don't have html_options, so you have to manually create the elements (using check_box_tag and radio_button_tag respectively).

Otherwise, because Rails tries to be helpful when dealing with unchecking checkboxes, you get weird behaviour when updating a project, along the lines of getting extra, but blank, tasks.


33. Ryan Bates Oct 17, 2007 at 16:56

Thanks for the comments everyone. I'll try to answer some...

@TheCompWiz, there's no need to call update_attributes here because the attributes have already been set earlier. A simple save is all that's needed.

@Ted, yeah, I should have made the save_tasks method private. Good catch.

@Brandon, the show_title? is another helper method which just checks an instance variable I set earlier. Here's the code:
http://pastie.caboo.se/108319

@magistrator, I wish there was a way to disable this automatic validation message but I'm not sure how. Instead what I have done is built a custom error message handler which I can instruct to ignore certain validation error messages.

@Michael, I'll try to put up the full project source code in a day or so.

@Gilles & Darryl, I wish I had an easy answer for the date select or checkbox fields not working, but unfortunately I don't. You'll have to make them manually.

On top of this, checkboxes are tricky because the browser only sends them if they are checked. This means they won't be organized into the proper task attribute hashes. You either need to keep track of them separately or use some javascript to set a hidden field when they are changed.


34. Joe Oct 17, 2007 at 21:59

I am having problems with validations on the task attributes on update. All the validations work wonderful on create. But for some reason, and I am assuming it is because the task_attributes=(attributes) method just assigns all the attributes to a task on update. How can one make sure the same validations pass for a task on update of a project when the task attributes are directly assigned?


35. Joe Oct 17, 2007 at 22:03

I also forgot about the after_update :save_tasks method where the save is set to false. Any ideas?


36. Joe Oct 18, 2007 at 02:09

Found the problem. validates_associated :tasks in the project.rb file. It is missing from the source listed above, but I was able to catch the error watching the video again. Thanks.


37. Rich Thornett Oct 18, 2007 at 07:09

Great stuff, Ryan, thank you!

My question - what about transactions? In this simple example, it's probably not a big deal if there's an error saving tasks after the project has updated, but in general, I want all of my models to be saved or none (if failure). The rails before/after methods are convenient, but I see them more as ways to fulfill pre- and post- conditions v. implementing core save operations. I'm in the midst of implementing a multi-model save now and have overridden save/save! to handle the logic of saving all the models in a single transaction. I'm not sure this is the best way, but it allows me to wrap all model updates in a single transaction.

We use acts_as_ferret, which is very slick and convenient but makes me nervous for similar reasons - indexing occurs after save, so if indexing fails it doesn't rollback the database. Thus the index can get out of sync with the model.

These are big issues in Java land, but I rarely hear them talked about in the Rubyverse. Was curious if anyone else shared these concerns. Perhaps they are simply not as important to the types of apps that Rails developers tend to build?

+1 on any future episodes dealing with associations, errors or validation for multi-model save operations. This seems to be a corner of Rails where good practices aren't well documented (if even established).


38. Jason Jason Oct 18, 2007 at 07:22

Quick question:

Why handle the destroying through javascript and hidden fields? Why not quickly trash it at the scene via ajax?


39. Ryan McGeary Oct 18, 2007 at 07:39

@Jason Jason, By handling the destroy through hidden fields, it gives the user the opportunity to Cancel all changes. Nothing persists until the form is submitted which makes for a nice user experience. Of course different apps warrant different behaviors.


40. Ryan Bates Oct 18, 2007 at 10:03

@Joe, sorry about that, source is updated to include that line now.

@Rich, I haven't tested this fully, but I believe if an exception is raised in a callback (save_tasks method) then it will properly rollback the save of the project and other tasks. Is it not working for you?


41. john Oct 18, 2007 at 11:59

Ryan,
Validation and error displays are tricky. Maybe a collection good practices can serve as a valuable episode.

Here're some questions:
1. validates_associated on tasks is nice: what if I want to create a blank project without any task? I don't want it preventing me from creating a project; and later I want to edit this blank project and add tasks.

2. Will "task.new_record?" in the partial fails in a blank project case (no task) if you try to edit it, since task will be a nil object?


42. Gilles Oct 18, 2007 at 12:37

For those who are dealing with date_select tag with this method of updating related models in one form, one easy workaround i have found is to use a text_field instead of the date_select to enter your date data … it's not a perfect one, but …
You can also do it by hand by usind select_date_tag and so on …


43. Gilles Oct 18, 2007 at 12:55

One last question for me about this episode : Is it possible to use this technique with HABTM relationships ?


44. Rich Thornett Oct 18, 2007 at 13:14

Thanks for the feedback, Ryan, I think you're right. It never occurred to me that save() and the accompanying life-cycle methods were wrapped in a transaction w/in the framework, but after your comment I did some experimenting and that appears to be the case. (Adding an after_save method that raises an exception and calling save() rolls back a model update that otherwise saves successfully.)

It's news to me (good news, I think, though I'm still digesting the implications) that ActiveRecord is running save() + life cycle methods in a transaction. Will have to dig deeper into the ActiveRecord code - thanks for the insight!


45. Skyblaze Oct 18, 2007 at 13:33

Anyway i still think that short particular arguments like this are better explained in mini-pdf. Will you release them in the future?


46. Ryan Bates Oct 18, 2007 at 15:50

@john, if the project has no tasks then it is treated as an empty array so neither of the issues you mentioned should be a problem. But I haven't tested it extensively, does it not work?

@Gilles, I'm pretty sure you can adapt this to work with HABTM. One thing you'll have to do is decide how the removal process will work. Does it just remove the join or the end model? Everything else is very similar.

@Skyblaze, no PDF, but I'll be writing a few tutorials on railsforum.com in the near future showing this technique with possible additions.


47. Rich Thornett Oct 18, 2007 at 17:36

Just to close out the transaction discussion, this from the Rails doc:

Save and destroy are automatically wrapped in a transaction.

Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction depend on or you can raise exceptions in the callbacks to rollback.

http://caboo.se/doc/classes/ActiveRecord/Transactions/ClassMethods.html


48. Igor Oct 19, 2007 at 14:39

I think that we could simplify this example a lot when not worrying weather task is new or updated.
On each update we could first delete all existing tasks and then add all submitted:

def task_attributes=(task_attributes)
  tasks.clear
  task_attributes.each do |attributes|
    tasks.build(attributes)
  end
end

This way we could remove all marking of new and deleted records and save_tasks method.
We should probably had to mark tasks association as :dependent => :delete_all and add transaction in controller so we don't lose tasks if some validation fails.


49. chrisff Oct 19, 2007 at 23:22

@Gilles, for the date_select, use

:index=>''

rather than

:index=>nil


50. RainChen Oct 20, 2007 at 00:37

[quote]
after_update :save_tasks

  def save_tasks
    tasks.each do |t|
      if t.should_destroy?
        t.destroy
      else
        t.save(false)
      end
    end
  end
[/quote]

when @project.save, it automatically save the @project.tasks. Then Rails calling the callback save_tasks ?
Would the same task do update twice ?


51. chrisff Oct 20, 2007 at 01:19

For moving error messages from failed task validations up to the project error messages, I added this "after_validations" method to Project:

http://pastie.caboo.se/109132


52. Brandon Oct 20, 2007 at 03:39

More on complex forms and validation is very welcome. I'm trying to do the reverse of your relationship here. I'm trying to create a User that belongs_to a group. I've discovered how to do that with "build_group", but I need validate the created group before saving the user, if the user is creating a new group. Otherwise, I just want to validates_presence_of the group selected in a drop down. The whole validation system, with callbacks and conditionals, is pretty hard to get a good overview handle on.

I love your work. Thank you!


53. tonghoho Oct 22, 2007 at 03:42

what about adding task has many sub_tasks
In field_for what name I have to use
"project[task_attributes][][sub_task_attributes][]" is not work.


54. Carl Youngblood Oct 22, 2007 at 10:16

+1 for another screencast discussing error messages and validations in greater detail.


55. Ryan Bates Oct 23, 2007 at 14:07

@Igor, interesting idea. Destroying the tasks each time would probably work, however I hesitate to do this. There may be extra state (such as created_at) that we need to stay persistent. There might be other associations with the task that would be lost as well.

@RainChen, from my experience calling @project.save will only save the tasks if the project itself is new as well. This is why I only handle the callback on after_update. Although I haven't tested this extensively.

@tonghoho, I don't think having two "[]" in a form field name will work properly. Instead you'll have to keep track of the sub tasks separately. Sorry I can't go into detail.


56. AJ Oct 24, 2007 at 06:26

Thank you for the cast. Instead of complicating things with the destroy logic and doing all nasty stuff, how about a call to link_to_remote to destroy the element?


57. HappyCoder Oct 24, 2007 at 09:47

I have a little bit different situation. In this screencast we have project-task but I have task-project situation (contact-group in my case)

So, the form had been saving even though there were validation errors in Group. valid? saved the day

if @contact.group.valid? && @contact.save
            flash[:notice] = 'New contact has been created'
            redirect_to
         end

In the view:
<% if @contact && @contact.errors %>
<%= error_messages_for 'contact' %>
<% if @contact.group %>
<% @group = @contact.group %>
<%= error_messages_for 'group' %>
<% end %>
<% end %>


58. James Herdman Oct 24, 2007 at 10:00

This might be Edge Rails only, but instead of setting an instance variable for <tt>error_messages_for</tt> you can do this:

<code>
%lt!-- in _task.rhtml --&gt;
&lt;%= error_messages_for(:task, :object =&gt; task) %&gt;
</code>

I've noticed that this method really dislikes date-time select boxes.


59. James Herdman Oct 24, 2007 at 12:22

For future Googlers, I didn't find the real solution, but what I ended up doing was leveraging the Chronic gem to parse natural language date-time expressions.


60. Joan Oct 25, 2007 at 10:52

I've using the code from this episode, weirdly enough, no matter I try to remove a task or edit task value, the code "@project.update_attributes(params[:project]) " in project controller has always turned out to be false, therefore redirect me to the edit page, where the task values ain't changed. I've spent a day to figure out what's wrong, still got no clue.


61. Henry Oct 27, 2007 at 02:42

Let me just say Ryan: thou are the man. My need for this and your creation of these podcasts came hand in hand. Props. Also, thanks a lot @Ilya for the error fixes when using the latest edge.

@Joan: try 'puts params[:project]' before that line of code in your controller and then check your development.log to see what it's spitting out. If it's not normal, then my guess if that you have a form builder problem in one of your views. Good Luck!


62. Joan Oct 28, 2007 at 11:45

Thank you, Henry. I finally was able to found out the problem, it's actually caused by the failure of validation in task model.


63. steve Oct 29, 2007 at 10:12

nice cast, there is one bug though.

if you use the after_update instead of the after_save callback and you are creating a project for the first time (aka, not updating) and you create a task then delete it and then hit save the deleted task will still be created. this happens because save_tasks callback is not called on the create of the project model so you don't do the check for should_destroy?. one fix is to just use after_save instead of after_update, although i guess this results in a double save on the initial creation of a project.

another fix is to add a before_create alongside the after_update:

  before_create :bc

  def bc
    fields_to_destroy = []
    fields.each do |field|
      fields_to_destroy << field if field.marked_for_destroy?
    end

    fields_to_destroy.each {|field| fields.delete(field) }
  end

i think this is optimal because it won't even hit the db for new tasks on a new project that are marked for delete. but changing the callback to before_save is much simpler and less prone to bugs


64. steve Oct 29, 2007 at 10:19

sorry for any confusion: replace 'field' with 'task'

here is a better example:
  before_create :bc

  def bc
    tasks.dup.each do |task|
      tasks.delete(field) if task.marked_for_destroy?
    end
  end


65. Greg Oct 30, 2007 at 16:58

Steve, I believe that issue is actually handled by the logic in the view code:

<% if task.new_record? %>
  <%= link_to_function "remove", "$(this).up('.task').remove()" %>
<% else %>

When you're creating a new project, you just delete the entire field for the task rather than marking it for deletion.


66. Tim Oct 30, 2007 at 23:25

I have items that belong to a studyform. I am trying to save both an item name and and item type into each studyform. My _item partial has:
<% fields_for "studyform[item_attributes][]", item do |f| %>
 <p>Item: <%= f.text_field :name, :index => nil %>
Type: <%= f.text_field :type, :index => nil %>
<% if item.new_record? %>etc

The name saves into the items database but the type is always null. Any help would be appreciated.


67. Paul Damer Nov 01, 2007 at 11:26

Has anyone made this work with check boxes?

The helper inserts a hidden field after checkboxes to have it send 0 if the box isn't checked.

Unfortunately it means that when it is checked both the 1 and the 0 get sent and it thinks there are 2 sets of attributes to save. It can also mean the values get applied to the wrong rows of data.


68. Duncan Beevers Nov 02, 2007 at 02:14

You shouldn't use .detect to pull up the appropriate task. To keep the task associated with the Project, use the association finder method tasks.find_by_id(params[:id])

This way you avoid instantiating a lot of unnecessary task models. This is essential when a model may have thousands of sub-models.


69. Joe Nov 04, 2007 at 09:59

Does this still work with the latest Rails trunk? I get this error: Can't mass-assign these protected attributes: id


70. Nathan Youngman Nov 05, 2007 at 14:38

@Ryan: I may have an easier method of removing tasks, but I'll need to test it when combined with the concepts in this set of screen casts.

I'm using line items of an invoice, but I'll use your terminology. So I have a collection of @tasks from the database, and a new empty array: new_tasks. I loop through the posted params, searching for an existing entry in @tasks, which I then update. If not found, I build a new task instead. Either way I append that task to the new_tasks array.

When I assign the @project.tasks = new_tasks and save @project, Rails handles the removals for me, simply by the fact that they don't exist in the collection... smart! They don't exist in the collection because they weren't posted and added to new_tasks. So I have no need to mark for destroy or even call destroy.

I have a bunch of code in the controller, and I'm not using the virtual attributes you mentioned... so I'm really looking forward to combining these to methods and seeing if it all works!


71. Skyblaze Nov 06, 2007 at 05:37

For me it works perfectly but now i have to add several "third" models that are related to my second model. How can i do that. Anyway an issue with this code is the fact that we will have input fields with same ids...


72. Sarah Nov 07, 2007 at 15:43

Ryan -- Thanks for the screencasts. They are extremely helpful.

@Paul, I made it work with checkboxes. I used check_box_tag. I use the checkbox value in the model (in def choice_attributes) to set the value of the field I want set, rather than setting it directly with the checkbox, like so:

attributes[:correct] = correct_choice_id.include?(attributes[:counter_id]) ? 1 : 0

It's a workaround but it works (and work s for radios, too). Then on the form end, I manually check the box like so:

<% is_checked = choice.correct == 1 ? true : false; %> and render the check box this way:
<%= check_box_tag('question[correct_choice_id][]',choice.id, is_checked,{}) %>

(that's on edit -- on new, when I don't have question.correct.choice I have named the checkbox with a counter from within the partial, and run essentially the same thing. I'm not sure if this will be clear or not but it does work for me so hopefully it helps.
 


73. andy Nov 08, 2007 at 15:55

I've found a couple of issues here that maybe only affect me, but I'll post in case someone else experiences this.

First, the "should_destroy" attribute was showing up in the first item regardless of which item was removed. The issue was that the 'should_destroy' hidden fields had no value and so did not end up in the correct array. I switched to hidden_field_tag with a default 0 value and it's fixed.

Second, when passing strings with non-alpha chars the URL encoding for javascript was running once for each layer of the array. So '-n/a-' was becoming "-n%25252Fa-", I did a 3.times{ attr[:string] = URI.unescape(attr[:string])} within the task_attributes= method. Hackish, but it works.


74. Jose Boza Nov 15, 2007 at 07:58

I'm having the same problem as Paul, is there an easy solution anywhere?


75. Chad Nov 18, 2007 at 13:46

Thank you so much for putting this together. It was incredibly helpful! Please add my vote to your doing a part 4 on validation.


76. Michael Nov 18, 2007 at 22:17

Thanks a lot for this Ryan.

+1 for a separate validations screencast!


77. Kevin Baird Nov 20, 2007 at 06:31

Ryan -

Thanks for the great screencasts.

I was a bit confused by the workaround in the should_destroy? method, until I remembered that MySQL lacks a genuine BOOLEAN datatype.

With PostgreSQL, you can define a boolean field in your model (without a trailing ?), and a predicate method (with the trailing ?) is automatically there in your model. Handy.


78. Jason Whitfield Nov 21, 2007 at 15:52

Strange problem here..

I can edit and delete tasks with no problem, but when I try to create a new task (or several at once) the very first of the previously saved tasks gets wiped out. I copied Ryan's code line for line and am running this in Rails 2.0.

Anyone else face this issue?


79. Jason Whitfield Nov 21, 2007 at 15:53

ALmost forgot to mention, I only have this problem when I have the marked_for_destroy hidden field and js link in that partial. I remove them and can add tasks with no problem. Odd..


80. Abraham Nov 22, 2007 at 20:42

How do I reference the project[task_attributes][] fields for auto_complete_field functions... Does anyone have an example using auto_complete_field and fields_for?


81. Pascal Ehlert Nov 25, 2007 at 04:08

Awesome screencast series, thank you so much!

Only one question.. I'm trying to do this with a habtm relationship (possible topic for another screenie? ;)) and unfortunately it doesn't save the linking table when updating attributes. :(

Does anyone have an idea how to tell rails to manually save it?


82. Ryan Nov 27, 2007 at 07:19

Ryan, this was a terrific tutorial. I'd definitely be interested in seeing more on validations, etc.

I'm also curious as to how you might take a progressive enhancement approach. The current method seems to require Javascript. I tried using a TaskController to provide web services, but if I make it polymorphic (Project and ToDoList), all hell breaks loose... or at least, it looks really ugly.

I like the fact that nothing is lost until the entire project is saved, so I'd prefer to keep it that way, but adding and removing tasks becomes difficult without Javascript.


83. Gernot Kogler Nov 27, 2007 at 07:42

Thanks so much for another great screencast series. However, I found it a little bit complicated and cluttered with all these hidden fields, javascript and such. So I took a slightly different approach. First, I modifed the task partial so that it passes always a model id:

  <%= fields_for "project[tasks][#{task.new_record? ? 0 : task.id}]", task do |task_form| %>

Now I can determine which tasks to create, update and delete in the method task_attributes= like this:

  def task_attributes=(task_attributes)
    task_attributes.each do |id, attributes|
      if id.to_i == 0
        tasks.build(attributes)
      else
        task = tasks.detect{|p| p.id == id.to_i}
        tasks.attributes = attributes
      end
    end
    tasks.reject{|t| t.new_record? || task_attributes.has_key?(t.id.to_s)}.each {|t| t.mark_for_destroy}
  end

Works like a charm and keeps my views clean.


84. BurmajaM Nov 29, 2007 at 07:04

Hi Ryan.

I find this series very useful. I've implemented some stuff using the technique you've described and it works great, but I'm interested how could I make file upload using this very technique (gmail like upload). Can you help me please?


85. Scotty Nov 29, 2007 at 16:13

I found this railscast series to be particularly useful! After stumbling across Jamis Buck's post about moving associated creations to the model your screen cast really cleared up the concept for me.

Validations had me puzzled, but I finally figured it out. I've posted what worked for me in my situation over on my <a href='http://psifire.livejournal.com/119168.html'>Live Journal</a> page... since I don't have a real blog.


86. gorn Nov 30, 2007 at 11:42

@ Gernot Kogler: I think that your idea can not work - I have been experimenting quite a lot with it and I think that all duplicate named imputs will get lost (for example if you have twho or more new tasks). The trick of putting the new items to the array only works if there is bare [] in the name of INPUT field. SO I suggest changing your conditional so it gives empty string for new tasks.


87. Rene Dec 04, 2007 at 17:30

Ryan,

great tutorial.

I unfortunately ran into a problem while using the code. My Task model has more than one field (name, priority and estimated cost) and they all are required (i.e. there is a validates_presence_of rule for each of them).

The problem happens when the user updates an existing task. More precisely when the user deletes the content of one of the text field and then clicks the remove link for that task.

As you can guess, since the remove links just hides the task, an incomplete task is sent to the controller and since validation happens before the after_callback is called, validation fails, the task is not destroyed and the controller redirects to the update page (with an error saying that the task is invalid)

Fortunately, the solution is not too difficult, all we need is to add a method in the Task class that returns false if the task should be destroyed and the pass that method as an "if" parameter to all validation rules for the task.

def do_validation
  !should_destroy?
end

validates_presence_of :name, :priority, :estimated_cost, :if => :do_validation


88. Rene Dec 04, 2007 at 17:39

Ryan,

would it be possible to have an RSS feed for tracking the comments for each episode?

In some cases, the comments/questions and answers are quite interesting too.

Thanks again for the great work


89. pimpmaster Dec 04, 2007 at 18:50

+1 for Pascal's suggestion

habtm would rock for this


90. John Barton Dec 07, 2007 at 09:00

I have a patch that avoids the empty {} if you use the index => nil with a select tag at http://dev.rubyonrails.org/ticket/10416


91. thomas Dec 20, 2007 at 01:19

I also see URL-escaped characters on child nodes. Problem is I can't unescape them. They stay escaped for some reason. :/

rails 2.0.1


92. Scott Windsor Dec 20, 2007 at 15:03

I came up with a solution for checkboxes based on Ryan's suggestion of using javascript.

application helper
http://pastie.caboo.se/131145

application javascript
http://pastie.caboo.se/131144


93. AJ Dec 22, 2007 at 01:40

Your screencast helped a lot solving the problem which I previously solved in a rather complicated manner.
I have a question to ask.
What if my project has 3 different type of tasks ->
A task can be either a
 - File
   -File Upload(Model : File)
   -HTML ADD (Model : File)
 -Link (Model : Link)
Now, I want three links at the bottom of the project to add file/html/link where html and file use the same model.
Now, in case of add html i want to redirect to a WYSYWIG editor(displaying it in the same page could get dirty) from where user can write the html and save it. I think a popup would be helpful in this case. And, then saving would redirect to the same form, from where you can save the project. Any heads-up on how this can be implemented ?
+1 for screencast on this.


94. Tom Jan 09, 2008 at 05:51

Hi guys, this is a great tutorial! i have tried to get it to work with photos aswell but I can't seem to get it right. I am using a polymorphic photo table which may well be the problem. Does anyone have any suggestions on how i could get it to work?


95. Michael Jan 11, 2008 at 20:39

@Gernot Kogler:

What happens if you create more than one new task? i don't see how your code will work, the params parser is designed to ignore all same named parameters after the first rather than to collect them. That is how the checkbox hack works.


96. Clovis Jan 14, 2008 at 00:01

Im getting this error:
Symbol as array index

on def task_attributes

why this ?


97. Dan Jan 16, 2008 at 09:14

What kind of javascript is this?
"up('.task')"

I have never seen an "up" (or "next") function that moves through the HTML DOM and return an element of a certain class. It sounds useful. Can someone post an online reference for this function?


98. mrb_bk Jan 16, 2008 at 09:58

Does anyone have any insight into how to adapt this example for has_many :through associations? The build aspect of this example breaks it -- I've poste dabout it here -- http://railsforum.com/viewtopic.php?id=14580

any help would be awesome!


99. John Jan 17, 2008 at 05:32

Any thought about how to handle security related issue such as mass assignment? Is there a clean way to combine your approach with using attr_accessible?


100. Juan Jan 20, 2008 at 10:36

I follow the tutorial and everything worked great. My only problem is that with what I am I add a new "task"(picture in my case) it overwrites the previous picture. If I add more than one picture, it overwrites previous pictures and adds the new ones. Any ideas on how to fix this? it works great if I don't wrap the file_field in an if statement, but I really don't want it to show up before a picture that has already been submitted....just want the ability to remove. Thanks

<div class="picture">
<% fields_for "housing[picture_attributes][]", picture do |pic_form| %>
<p>
<% if picture.new_record?%>
Upload A picture:
<%= pic_form.file_field :uploaded_data , :index => nil %>
<%= link_to_function "remove", "$(this).up('.picture').remove()" %>
<%end%>
<% unless picture.new_record? %>
<img src="<%=picture.public_filename(:thumb)%>" />
<%= link_to_function "remove", "mark_pic_for_destroy(this)" %>
<%= pic_form.hidden_field :id , :index => nil %>
<%= pic_form.hidden_field :should_destroy , :index => nil, :class => 'should_destroy' %>
<%end%>
</p>
<% end %>


101. AdamD Jan 23, 2008 at 14:39

I'm really running into road block after road block trying to figure out how to get associated validation working properly. At this point all I can get is an error message that says 'Model is invalid' rather than the preferred child-model's attribute error message.

We really need a screen cast on how to get this kind of validation working.


102. bob Jan 28, 2008 at 03:44

FzrmBT great site thx http://peace.com


103. luu Jan 29, 2008 at 09:32

here's how I handle error msgs. If someone can suggest improvements, I'd appreciate it.

In this example: post = parent model, asset = child model

I check @post.errors. if it's not empty, I display some generic msg, like: you have one or more errors, pls fix then submit again.

@post.errors is an activerecord object, an array of attr=>msg like this

['title'=>"title can't be blank",'body'=>"body can't be blank',...]

Next, I display the error msg for each field right above it, like this

<label for="post_title">Title</label><br />
<%= error_msg_for @post, 'title' %>
<%= f.text_field :title, :size => 60 %>
<br />
<br />

<label for="post_body">Body</label><br />
<%= error_msg_for @post, 'body' %>
<%= f.text_area :body, :cols => 60, :rows => 15 %>

in my child model, asset, do similar thing

<% fields_for "post[asset_attributes][]", asset do |f| %>
<%= error_msg_for asset, 'name' %>
<%= f.text_field :name, :size => 15 %>
...
<% end %>

that is, display the error msg for each field in asset (child model). If you have a grandchild model, you can do the same probably.

finally, here's my method for displaying the error msg

def error_msg_for( record, field=nil)
return '' if record.errors.empty?
 if field
 a = []
 record.errors.each do |attr,msg|
  a << msg if attr == field
 end
 %(<div class="field_error">#{field} #{a.join('<br>')}</div>) unless a.empty?
else # if field == nil
 %(<div class="errorExplanation">There was a problem with the data you submitted. Please fix the error(s) below then re-submit.</div>)
end
end

so I can put

<%= error_msg_for(@post) %>

without a field parameter at the top of the form, it'll display the generic "you have an error" msg when there's any error in the parent or child.

you may notice I check for possible multiple error msgs for the same field. I don't know if this ever happens or not.

also, the field name needs to match exactly with the attribute. if you're not getting the error msg back as expected, dump out errors array to see what's in it.


104. luu Jan 30, 2008 at 00:36

Juan, I found for some reason it only works if you have a file_field for ALL the pics. So for the existing pics, I'd simply hide them

<%= pic_form.file_field :uploaded_data , :index => nil, style=>'display:none' %>

not pretty, but it works


105. Jamie Hill Jan 30, 2008 at 09:10

Has anyone come up with a way to get rid of the HTML validation errors rails creates with double underscores and non-unique id attributes when setting :index => nil ?


106. Dmitri Koulikoff Feb 07, 2008 at 15:20

partial: http://pastie.caboo.se/149027
In the log the array is messed.
"set_mappings"=>[{"file_name_pattern"=>"*pricelistsample*"}, {"format"=>"1", "file_name_pattern"=>"*availability*"}, {"format"=>"1", "id"=>"953125641"}, {"id"=>"996332877"}]

It looks like rails collect the array in a wrong way. What could it be?


107. Dmitri Koulikoff Feb 08, 2008 at 06:47

It is known problem which is not resolved in rails 2.0 and has the Ticket #10101
http://dev.rubyonrails.org/ticket/10101

Thanks for attention.


108. Bryan Richardson Feb 10, 2008 at 10:16

Hi Ryan,

Hopefully you're still reading comments for this Railscast. This series has helped me out with my current application, thanks!

One quick question: if I use a check_box in my partial when using fields_for, it always tries to submit two entries for every one displayed on the page. I'm assuming this is because the check_box helper adds a hidden field for when the box is not checked. Is there any way around this?

Thanks! -- Bryan


109. CarbonJoeTunis Feb 14, 2008 at 08:14

for those having the problem of tasks not being saved or deleted on Update. i had the same problem and i just tracked it down (i'm usin Rails 2.0 as well and didn't have to change anything from Ryan's example -- nice work by the way!). anyway, my issue was i inadvertently had a single = sign in the task_attributes method. i had this:

photo = photos.detect { |p| p.id = attributes[:id].to_i }

so an assignment was happening, rather than the correct:

photo = photos.detect { |p| p.id == attributes[:id].to_i }

that fixed my issue.


110. mercury Feb 15, 2008 at 06:38

I'm curious: what is the best way to limit the number of fields the user can add during both create and update? For instance in my application I'm using this to add picture upload fields, but a user has a max of 9 pictures to they're allowed to upload.

As a side note: Loved these 3 Eps! Thank you so much for taking the time to demo RoR to the masses. Many blessings to you as you work toward your 100th episode!


111. Eddie Feb 20, 2008 at 07:02

I'd love to see how validation errors are highlighted in fields that are dynamically added to the form - if you added some task fields and these failedvalidation, when the default view for the form is reloaded (to highlight the errors) the additional fields that were added in the previous step (that failed validation) wouldn't be there...


112. Harlan Crystal Feb 21, 2008 at 09:03

Is there any strategy for using this technique with radio buttons? The problem being that instead of having separate "button groups" for each task, you end up with one huge button group for all the radio buttons across tasks, because they all use the same "name" for radio widgets in each task.


113. Shawn Banks Mar 06, 2008 at 04:49

 am wondering if someone may be able to point me in the right
direction.

I have a survey that I am trying to do that has in it many one too
many relations. Thanks to railscasts complex forms series, I was able to
get them all working. I have come to a road block and hoping someone
can help. The survey capture a companies profile, where things get sticky
is
I now need to create a relationship that will allow a user to enter in
categories that the company might sell products in. Here is the
predefined list.

* Wood based panels
* Complete pre-fabricated buildings and houses
* Wood kitchen cabinets
* Wood windows and doors
* Wood flooring
* Millwork
* Mouldings
* Engineered wood products
* Other wood based building products
* PVC windows
* Steel doors
* Insulation
* Stone-processed products
* Basement envelope systems
* Other non-wood building products

This list is set in stone and no others can be added by the user. So a
company can have many categories.

What I started was created a first created two tables(categories and categories_companies) in addition to the original company profile(companies).

I created the models and put in them the following lines:
model categorization

   1. belongs_to :company
   2. belongs_to :category

model company

   1. has_many :categorizations
   2. has_many :categories, :through => :categorizations

model category

   1. has_many :categorizations
   2. has_many :companies, :through => :categorizations

I am now stumped. I am really new to this and object orientation programming.

I think that I should in the company controller have a statement to grab all the categories from the DB and display them in the new company view form to allow a company to pick which categories they sell in.

Also I need to add a relationship to allow users to add products to each category with details on each
product.

I am not sure on how to approach this as well I am very new to Rails.
I come from a coldfusion background. If it would help, I could send you
any existing code I have.

I hope someone can help, Thanks in advance!


114. Kevin Triplett Mar 12, 2008 at 17:03

@chrisff

Your association validation error message pastie rocks!

Here's an enhancement to it that strips out the useless "is invalid" error messages so you're only left with the useful association validation messages:

http://pastie.caboo.se/165073


115. Shawn Mar 14, 2008 at 08:35

I am having the same issue as Harlan Crystal I am using Radio buttons but they are being all grouped into one big group


116. Skalbert Mar 15, 2008 at 08:24

Thanks a lot!


117. Jesus Bernardo Ruiz Mar 25, 2008 at 12:22

Hi i have the following problem. I followed the screen cast to create
complex forms from railscats so i create a partial view for my
equipments list, so the client can add several different equipments, I
also made another view in case looking for the equipment inside the
select box. When i choose a equipment from the view (find_equipment) i
created there is no problem when there is only one equipment inside
the client form, but when I another equipment and I try to select it
from the select view (find_equipment) it does not update the select
box it should, it update the first select box.

I have a similar problem with observe_field, it should update the
properties for a certain product, but it only update the first select
list the other lists are not updated.

Here is the source code:

_equipment_services.html.erb
<div class="equipment">
  <% fields_for "voip[equipments_services_attributes]
[]",equipment_services do |e|%>
  <p class="titulo">Informaci&oacute;n Equipos</p>
  <p>
    Activo Fijo
    <
%=e.select("equipment_id",Equipment.find(:all,:conditions=>"estado=6",:orde­r=>"numero_activo_fijo").collect{|
p|[p.numero_activo
_fijo, p.id]},:index=>nil)%>

        <% if equipment_services.new_record? %>
          <%="Estado Activo"%> <%=e.hidden_field("estado",:value=>6)
%>
          <%= link_to_function "Eliminar",
"this.up('.equipment').remove()" %>
        <%else%>
          <=% link_to_function "Eliminar",
"mark_for_destroy(this,'.contact')" %>
          <%= e.hidden_field(:id,:index=>nil) %>
          <%= "Estado" %>
          <%=
e.select("estado",Code.find(:first, :conditions=>["id=5"]).elements.collect{|
p| [p.descripcion,p.id]},:index=>nil) %>
          <%=
e.hidden_field(:should_destroy,:index=>nil, :class=>"should_destroy")
%>
        <%end%>
        <%=link_to "Crear Nuevo
Equipo",add_equipment_equipment_path,:popup =>
["Agregar_Nuevo_Equipo","height=600, width=700"]%>
        <%= link_to "Buscar Equipos",
{:action=>"find_equipment", :controller=>"show_select"}, :popup =>
["Show_Equipment","height=60
0, width=700"]%>
  </p>
<%end %>
</div>

javascript function
function set_value(id,value)
{
  opener.document.getElementById(id).value=value;
  window.close();

}

find_equipment.html.erb

<table>

  <tr>
    <% form_tag find_equipment_show_select_path, :method=>"get" do%>
      <td>Buscar Usuario:</td>
      <td><%=select_tag "busqueda","<option value='1'>Modelo</
option><option value='2' selected='true'>N&uacute;mero Activo Fijo</op
tion><option value='3'>N&uacute;mero Serie</option><option
value='4'>Direcci&oacute;n MAC</option>"%>
      <td><%=text_field_tag :search, params[:search]%></td>
      <td colspan=3><%=submit_tag "Buscar", :name=>nil%></td>
     <%end%>
  </tr>
<tr>
  <th>Modelo</th>
  <th>Marca</th>
  <th>Activo Fijo</th>
  <th>N&uacute;mero de serie</th>
  <th> Tipo Equipo </th>
</tr>
<%
    switch=false
    for equipment in @equips
%>
  <tr class="<%= if switch
                   "TRPAR"
                else
                    "TRIMPAR"
                end %>
  <% switch=!switch %>">
    <td><%=equipment.modelo%></td>
    <td><%=Code.getDescripcion(equipment.marca)%></td>
    <td><
%=link_to_function("#{equipment.numero_activo_fijo}","set_value('voip_equip­ments_services_attributes__equipment_id','#{equi
pment.id}')")%></td>
    <td><%=equipment.numero_serie%></td>
    <td><%=Code.getDescripcion(equipment.code_id)%></td>
  </tr>
    <% end %>
</table>

Can anyone help me please? i'm kind of lost here


118. David Blankenship Mar 26, 2008 at 06:27

Great series! I did have one question about the Complex Forms parts, and that is how to degrade if javascript is disabled. Is there a way to allow link_to_function to "submit" to another url so you can recreate the objects and then add a new task and render the original form or am I thinking about it all wrong?


119. Ryan Bates Mar 29, 2008 at 15:39

Thank you everyone for your excellent comments. Sorry I haven't gotten around to respond to them all.

I just updated the code in the notes to use a better approach. This is taken from the recipe I wrote for Advanced Rails Recipes. Hopefully it will solve the majority of these problems.


120. Ryan Bates Mar 29, 2008 at 15:57

@David Blankenship, good question. I would probably hide the link when javascript is disabled, then you could handle adding tasks through a non multi-model form by going through a tasks controller "new" action for each task after the project has been created.


121. Jeff Dean Apr 05, 2008 at 14:11

I've blogged about how to get around the issue of the radio buttons having the same ids at "http://zilkey.com/2008/4/5/complex-forms-with-correct-ids":http://zilkey.com/2008/4/5/complex-forms-with-correct-ids


122. Chris Apr 05, 2008 at 15:23

The code you've posted here isn't the same as that in the Advanced Rails Recipes book (e.g. the task partial is significantly different). I'm wondering which code is more up-to-date, particularly in the light of errata item 31586 posted at the book's site.

Many thanks for your very fine Railscasts.


123. Ryan Bates Apr 08, 2008 at 23:50

@Jeff, thanks for posting this! I included the link in the show notes.

@Chris, good eye. I modified the code slightly from the Advanced Rails Recipes book by moving some into the "fields_for_task" helper method. For simplicity, the recipe in the book does this inline in the view using a local variable, but recommends moving it into a helper method. Since that note wasn't included here I wanted to encourage the best practice of not setting variables in the view. The code itself still does the same thing, it's just moved around a bit.


124. Scott Vesely Apr 28, 2008 at 07:52

I have, probably, a dumb question:
For forms such as this, where we're adding multiple tasks at once to a new form, I understand (I believe) that they're adding new post variables like project[]task[] etc, and editing existing values like project[1]task[3] etc.

My question: wouldn't it be clearer, and perhaps more semantically correct, to ALWAYS assign a sequential integer as the array index (so that new record combinations might also look like project[4]task[2]) and simply rely on a rested id tag in order to identify which records have previously been saved (existing) and those that lack an id (new)?

I'm sure I'm missing something, but it just feels like that postulated format ties in better with the object model by utilizing the correct location for the id data, and relieves the problem of the post parser having to make educated guesses as to which data pairs should be grouped into an individual object.


125. Dan Apr 29, 2008 at 13:23

Is there anyway for fields_for to look at the scope of the enclosing form_for and change it's scope accordingly?

Depending on the form, sometimes I need the fields_for to be scoped as

"parent[object_attributes]"

other times I would like it to be

"parent's parent[parent][object_attributes]"

I have been passing it around to the partial using :locals, but it seems like there must be a better way.


126. Matt Apr 30, 2008 at 11:04

Ryan - thanks for the great screencasts - I hope you keep them coming!

WARNING: Solution doesn't work with Edge Rails (2.0.3) when Task has nested fields! (Unless I'm not building the names of the inputs correctly... but I'm fairly certain I am!)

I've found this (Rails2) solution has problems when Task itself has nested fields - at least in Edge Rails (2.0.3). The request parameter parser won't parse the request into the new_task_attributes array correctly.

Example: Let's say Task has_one :person, and Person comprises fields 'first_name' and 'last_name'. You'd probably end up with a form input similar to the following (of course you'd choose what the actual name of the attribute 'person_attributes' would be):

project[new_task_attributes][][name]=taskname1
project[new_task_attributes][][person_attributes][first_name]=person1-first
project[new_task_attributes][][person_attributes][last_name]=person1-last
project[new_task_attributes][][name]=taskname2
project[new_task_attributes][][person_attributes][first_name]=person2-first
project[new_task_attributes][][person_attributes][last_name]=person2-last

Edge Rails (2.0.3) creates an very wrong parameters hash for this:
 "project"=>
  { ...project-data...,
   "new_task_attributes"=> [
      {"person_attributes"=>{"first_name"=>"person1-first"}},
      {"person_attributes"=>{"last_name"=>"person1-last"}, "name" => "taskname1"},
      {"person_attributes"=>{"first_name"=>"person2-first"}},
      {"person_attributes"=>{"last_name"=>"person2-last"}, "name" => "taskname2"}
     ],
   }

Just a warning to folks out there who are trying this solution, but have nested fields as well.


127. Thalas May 01, 2008 at 22:13

While everything works, I am not able to update tasks using Firefox. I can in IE! I can create new records though! Any ideas what I need to check? Thanks.


128. Dylan May 19, 2008 at 16:45

In the revised example I found that I had to use the index option on my fields when I was specifying a to_param method for my equivalent Tasks model


129. Roy May 20, 2008 at 14:28

Genius code--thanks so much!

That said--is the after_update callback absolutely necessary?

It looks to me like it works if you call task.save from within Project.existing_task_attributes= (right after the line "task.attributes = attributes").

Is that right?


130. kino May 23, 2008 at 01:57

It becomes evident that multiplicities of the transcendental-phenomenological reduction exist for us thanks to, by virtue of a synthesis in which what is meant coincides and agrees with the stream of noematic descriptions, noetic acts.


131. Jose May 23, 2008 at 15:17

Hello Ryan,

I have used this sublime recipe as the basis for a kicked-up-a-notch version, with a more complex database schema and it's working perfectly so far.
The point where I'm having problems is if, for instance you also have a Category model that has_many :tasks and you have to select which category the task :belongs_to before clicking the add_task_link.
What I have tried -unsuccessfully- to do is when you call page.inset_html, to pass in something like :object => Task.new(:category_id => what_should_i_put_here?), being (as you might have figured out) "what_should_i_put_here?" the main problem. I can get the category_id value from the form field, but I can not insert it within the page.insert_html method.
Thank you.


132. bijou May 29, 2008 at 02:29

the codes didn't work for me well (updated version or no) and i guess i can blame the complexity or disorderliness of my project code. with the help of a friend, i was able to find a way around my predicament with these complex forms. i use collection_select instead of text_fields too. anyway, while my friend was a genius being able to figure out how to do the updating (edit or adding a new record) of multiple forms on the spot, he wasn't able to figure out how to delete records (plus he had somewhere to go to).

but wow, i still can't believe how i managed to figure out a very simple way to do it. it's a bit of simple common sense, actually.

i used the javascripty "remove" in conjunction with the server side's "destroy".

<% if task.new_record? %>
<%= link_to_function "Remove field", "$(this).up('.task').remove()" %>
<% else %>
<%= link_to 'Cancel task', task, :confirm => 'Are you sure?', :method => :delete %>
<% end %>

hope that will help anyone...


133. Lucas Castro Jun 02, 2008 at 15:01

Ryan, i am getting an weird error when i combine the use of "fields_for" and "collection_select" at the partial code.

There's someone having the same issue, any tip?

http://railsforum.com/viewtopic.php?id=18598


134. Kevin Whinnery Jun 09, 2008 at 09:29

The excerpt from the book on this topic was insanely helpful. Thanks for all the good work on this and other screencasts and reference materials. I've learned more about Rails from you than any other single source, hands down.


135. victor thompson Jun 13, 2008 at 15:53

@Scott, and everyone fighting the checkbox problem with dup fields. I fixed your code. The else if in the javascript didnt run right and I found it more suitable to remove the name field of the hidden field rather than attempt to toggle its value--which did not seem to work.

My solution also allows you to still use check_box instead of check_box_tag. The only mod to the form is to add :onchange => "toggle_name(this);" to the checkbox form element. Then simply declare the toggle_name function in your appplication.js file as such:

http://pastie.org/214828


136. victor thompson Jun 13, 2008 at 16:34

The code I posted does not appear to work properly under IE due to its nextSibling implementation. I'll try to find a more robust solution on monday.


137. Pege Jun 18, 2008 at 08:30

This works great but I have a problem. My partial is an invoice item field with amount, price and total sum (and of course there can be many of these). I'd like the total sum to be calculated by Rails when amount or price is changed. Because the form fields have no unique id's, I cant' get this to work with remote_function.

It works if I calculate with JavaScript by traversing trough the DOM like so:

<% item_form.text_field :amount, :onkeyup =>
";$(this).up('.invoice_item').down('.sum').value=$(this).up('.invoice_item').down('.price').getValue()*$(this).getValue();" %>

But I can't use the $(this).up... thing in remote_function :update => ... and can't use RJS because there's no unique id for the fields.

Is there any solution for this?


138. Harry Jun 20, 2008 at 17:02

I'm using Rails 2.1 and the updated code above and getting "undefined method `new_record?' for "1":String" when I update a record.

The error comes from this line in the #existing_task_attributes method:
tasks.reject(&:new_record?).each do |task|


139. Harry Jun 21, 2008 at 09:07

All good. The error was mine.


140. Seth Jul 01, 2008 at 13:25

Great article and a lifesaver BUT.. when you start doing things like time_select the custom forms_for_x is no longer rendered properly (namely the hidden fields generated by this time_select).. Not sure if this would effect other rails tags.. if so it's not as versatile as once thought and would have a limited use.


141. John Jul 02, 2008 at 11:30

On rails 2.1 - updates I get:

can't convert String into Integer

app/models/poll.rb:47:in `[]'
app/models/poll.rb:47:in `existing_choice_attributes='
app/models/poll.rb:46:in `each'
app/models/poll.rb:46:in `existing_choice_attributes='
/Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/base.rb:2361:in `send'
/Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/base.rb:2361:in `attributes='
/Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/base.rb:2360:in `each'
/Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/base.rb:2360:in `attributes='
/Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/base.rb:2261:in `update_attributes'

If I update existing_choice_attributes's choice.id.to_s to choice.id it works, but will destroy my choices when updating the same form twice.


142. AJ Jul 04, 2008 at 06:04

BUG: In case number of attributes is more than one in the partial, browser does not groups it right and sends wrong data.
Example:
Data expected :
new_part_attributes[]["ordering"]=>"1",
new_part_attributes[]["inline"]
=> "false"
new_part_attributes[]["wfile"]=>#<ActionController::UploadedStringIO:0xb602ea20>

new_part_attributes[]["ordering"]=>"2"
new_part_attributes[]["inline"]=>"true"
new_part_attributes[]=><ActionController::UploadedStringIO:0xb602cd38>

Data Send By Browser:
"new_part_attributes"=>[{"ordering"=>"1", "inline"=>"false", "wfile"=>#<ActionController::UploadedStringIO:0xb602ea20>}, {"ordering"=>"2", "inline"=>"true"}, {"wfile"=>#<ActionController::UploadedStringIO:0xb602cd38>}]

Possible solution is send index also in the array, so that it groups it right.


143. AJ Jul 04, 2008 at 07:41

And also, what about doing the same thing for Gmail type file upload ?

In that case, your previous code, in which you used to set should_destroy? would work better.


144. Marjan Jul 11, 2008 at 14:26

I get the same "can't convert String into Integer" error as John (3 posts above)

Any help would be much appreciated.

Thanks


145. Marjan Jul 11, 2008 at 14:56

Umm, found the solution to the problem above.

Looks like I forgot to remove the ":index => nil" part from the task helper


146. Dominic Amann Jul 17, 2008 at 19:25

Ok, so I bought the book, and keyed in the example except I changed the names of tables to fit my project. It didn't work. I beat around the bushes for a couple of days, but couldn't find my bug.

Then I keyed it in using the exact line for line coding from the bug. Still same problem.

Then I found this page, and downloaded the code.

After adding a named route for projects (the code does not have it - I had to figure this part out), it still doesn't work. I get no errors, just neither my project nor my tasks ever get saved.

Has anyone else actually got any of this to work? I mean I would hate to be completely wasting my time - I could code it the hard way in far less time than I have already wasted here.


147. Dominic Amann Jul 17, 2008 at 19:41

I am pretty sure my problem is in that routing thing that is not provided in the code, railscast or the book. Perhaps the version he uses has different defaults for routing or something. Anyhow, I have wasted 5 good coding days on what looked like a good idea, but just won't work for me at all - and the logs give no clues that I can use.


148. Dominic Amann Jul 22, 2008 at 15:36

Ok, so I got it working by mashing together some code from above into the code in the book, and replacing all the instances of project_path with actions and ids as appropriate in my controllers and views.


149. mellifluidic Jul 22, 2008 at 16:16

@Dominic: I (after a bit of futsing with mistyped fields) have indeed gotten this to work for the simple model used in the railscast. What version of Rails are you running?

@All: While implementing this technique with my own application, I got through the first and second casts in this series without problem, but when I went through the steps in this one, I now get the following in my log:

/!\ FAILSAFE /!\ Tue Jul 22 15:49:37 -0700 2008
  Status: 500 Internal Server Error
  Conflicting types for parameter containers. Expected an instance of Hash but found an instance of Array. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value.

I'm gonna keep digging and see if I can figure out why something is getting sent an array that is supposed to be a hash (my guess is that it is the params hash), but I figured I would drop a line and see if anyone could quickly spot my problem here. I'm still pretty new to Rails, so any help is appreciated.

Thanks


150. mellifluidic Jul 22, 2008 at