RFC: Improving the rack parameter normalization

andy
Fri, 07 Aug 2009 03:06:36 -0000

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