Re: RFC: Improving the rack parameter normalization

andy
Fri, 07 Aug 2009 16:57:46 -0000

I''ve got a slightly modified form of this working.

The rules for parameter names in my scheme are:

- may optionally begin with an unbracketed name
- thereafter must be a sequence of bracketed names
- any bracketed name starting with ''.'' must be immediately
followed by
one that does not
- the whole thing may optionally be terminated with ''[]''

A "bracketed name" in the above is just any string that
doesn''t
include ''['' or '']''
characters, surrounded by ''['' and '']''

My replacement normalize_params implementation in rack/utils.rb checks
the parameter name it''s initially called with to see if it meets these
criteria.
Moreover, I''m pretty certain that any name that follows the rules but
does not
have any bracketed names beginning with ''.'' will yield the
same
results via
either the old or the new function, so what I''ve done should be
backward
compatible.

The new scheme uses the dotted names to introduce arrays of hashes.
The
part of the name following the ''.'' is saved discretely in the
last
hash of that
array (under a key named ''.index''), and a new hash is
allocated
whenever
the current dotted name is different from the one that was most
recently
stashed. The stashed .index keys are all removed before the parameter
structure is finally returned, so they won''t screw up attributes
setters.

This scheme means you don''t need to worry about using
''+xxx'' for the
first
parameter in a group, the rest of which will use ''.xxx''
instead, as
per my
original proposal.

With these changes in place (plus a small change to acts_as_list to
avoid
assigning a position value to a new record if one has already been
assigned), my original problem is very easily solved, as follows:

In my :task partial template, I use:

    <%= project_form.fields_for :tasks, task,
                :child_index=>".#{task.id ||
''REPLACE_ME''}" do |tf| %>

My "add a task" helper needs to replace ''REPLACE_ME''
with a unique
value via javascript (as is already common practice in these dynamic
multiform scenarios).

This yields parameter names in the subform like:

   project[tasks_attributes][.13][name]

Then in my projects controller, my update action is defined like this:

  def update
    params[:project][:tasks_attributes].each_with_index { |ta,i| ta
[:position] = i }
    @project = Project.find(params[:id])
    ...
  end

That''s it.  I''m actually thinking now that, since
it''s so
straightforward
to elect to use this feature (you just need to supply a :child_index
that
starts with a dot in the nested fields_for call), it may not require
any
changes to the form builders as I had originally expected.

I haven''t yet tried this with more deeply nested forms, but I will do
so
shortly.

If anyone has any comments or recommendations, please post. I''d like,
soon, to publish a patch for this to be incorporated into the rails
distro.

Thanks

-andy

On Aug 6, 11:06 pm, andy <andyrlo...@gmail.com>
wrote:
> 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