I have struggled much of the day today to do something that on the surface seems like it should be a piece of cake. But either it's not, or I'm too dense to see the easy way to do it.
I'm leaning toward modifying the rack parameter parser to (a) make it far cleaner and more intelligible than it currently is (including documenting exactly what it does!), and (b) make it easy to do what I'm after. I think I can do this in a mostly backward-compatible fashion, and could provide a compatibility mode if needed for anyone who needs the old behavior.
I'd appreciate any feedback the experts may have before I embark on this.
Here's a simplified version of what I'm trying to do that ended up being (I think) impossible without resorting to some javascript that really should not be required:
Say I've got a nested model structure that looks like this:
class Project < ActiveRecord::Base has_many :tasks accepts_nested_parameters_for :tasks end
class Task < ActiveRecord::Base belongs_to :project end
I'd like to be able create an edit form for :project that includes a dynamically varying list of :task subforms. I'll have a delete button next to each :task subform, and an "Add a Task" button at the bottom of the last. All fairly standard and not very hard, especially with the new accepts_nested_parameters_for feature.
But now here's the kicker: I also want to use the "sortable_element" helper so that the user can reorder the tasks - including newly added tasks - within the project. In order to save the final ordering, this means I need to be able to detect, from the request parameters, the order that the :task subforms appeared in the overall form at the time it was submitted. This should be easy, because browsers are required to send form parameters in the order in which they appear at the time of submission, and sortable_element works by physically reordering the subforms in the overall DOM structure.
The problem is that rack loses this ordering information, and I'm pretty much convinced that it can't be made to keep it. You can ALMOST get this to work by specifying :index=>'' and :child_index=>'' in all your fields_for calls. But this results in illegal HTML (strictly speaking, though it works, at least in the browser I've tried), and it's also slightly risky. The illegal part comes from the fact that you'll end up with multiple input elements in your form that have the same id value. E.g. a text field for task 'name' would appear in every :task subform, and in each appearance it would carry the same id value. And not only would that be illegal, it would also make it impossible to have per-subform radio button groups. The radio button input elements would all have the same name across all subforms, so they'd all be linked, rather than only within a subform.
Even without those problems, the strategy is dangerous because of the way rack detects when it's time to start a new hash, rather than continuing to fill a current hash. This comes into play for a parameter with a name like 'project[tasks_attributes][name]'. The the empty brackets call for an array of hashes at that point in the structure, and rack will look at whether 'name' appears as a key in the current last element of that array. If so, it opens up a new element in the array, otherwise it adds to the current last element.
This mechanism will fail if not all your subforms offer the same set of fields. E.g you might have separate buttons for "add quick task" and "add task," both of which add the same sort of object, but "add quick task" provides an abbreviated form for easier entry. You'd have to add the other fields as hidden fields in the abbreviated subform in order to be safe, and it'd probably be a hellish debugging exercise to figure that out!
OK, so here's my idea:
1. When "" appears in a parameter name it always indicates an array at that point in the structure. What fallows goes into a new element at the end of the array, using logic equivalent to what is currently provided by rack. 2. When "[.index]" appears, what follows will be be added to the hash that is the current last element of the array. The leading dot prevents this index from itself being a hash key in what would then have to be a containing hash, rather than a containing array, so ordering of the substructures is preserved. 4. When "[+index]" appears, a new hash is added to the end of the array, and what follows goes into that hash. 5. When [index] appears in a parameter name (without leading dot or plus), the index is treated as a hash key for what follows same as current behavior.
Here's an example:
project[title]='My Project' project[tasks_attributes][+0][id]=1 project[tasks_attributes][.0][name]=Sleep project[tasks_attributes][+1][id]=5 project[tasks_attributes][.1][id]=Sleep more project[id]=3
The params structure would look like this: {:title=>"My Project", :id=>"3", :tasks_attributes=>[{:id=>"1", :name=>"Sleep"}, {:id=>"5", :name=>"Sleep more"}] }
The current rake parameter processor would yield (assuming dots and pluses are removed from the above): {:title=>"My Project, :id=>"3". :tasks_attributes=>{"1"=>{:id=>"5", :name=>"Sleep more"}, "0"=> {:id=>"1", :name=>"Sleep"}} }
With the new structure it would be simple matter for the controller to take a pass through params[:project][:tasks_attributes] and assign :position values for the benefit of acts_as_list in order to retain the final subform ordering. With the current structure, one would need to use a javascript callback before submit to add those values to the DOM, or AJAX calls to update positions every time something gets reordered. Neither would be all that difficult, but it's infuriating, all the required information is already available for free in the raw form submission.
To make this really work well, the form helpers would also need to be modified to make use of the enhanced naming scheme. If I do this, my goal will be to do both the enhanced rack parameter parser and the form helper modifications to take advantage.
Thanks for any comments or recommendations.
-andy