Taking the Pain Out of Complex Forms in Rails
The other day I was discussing Rails’ form processing behavior with Ben, when the topic of editing multiple associations in a single form came up. Effectively, he needed to be able to manipulate a collection of records associated with a parent via a has_many association. For each item in this collection, he wanted something like two drop down lists — nothing unreasonable. He said it was similar to recipe 13 from Advanced Rails Recipes — “Handle Multiple Models in One Form” — except that he had to use two fields instead of the one textfield, and it was proving to be surprisingly tough.
I know what you’re thinking: surely this can be extended to more than one form field per item without any problem? Up until I read the code myself a few months ago, I wouldn’t have thought it would be so difficult either — but it’s true. The parameter parsing code is quite quirky and, without reading the detail of the source, borderline unpredictable. I’ve had a few ideas for dealing with more complicated forms in Rails ever since I first ran into this issue. Only recently have I actually gotten around to doing something about it. Here’s an abridged version of an email I just sent to the rails-core mailing list:
Hi,
…
A colleague of mine recently ran into a related problem when dealing with collections of records in Rails forms, which prompted me to finally do something about the parameter parsing behavior. Now, while I’m sure we’re not the only ones hitting this wall, I’m aware that proposals to change to the parameter parsing semantics in Rails is likely to be met with a little caution, hesitation — possibly even terror.
With that in mind, I’ve put up a plugin so everybody can give this a go without having to apply any nasty patches:
http://www.vector-seven.com/git/rails/plugins/form_collections.git
Please see the README (attached) for the full details.
…
Cheers,
Tom
This is a plugin which greatly simplifies the existing Rails parameter parsing code, and makes it a whole lot easier to deal with collections in your forms. I encourage all you Rails developers out there to take a look when you get the opportunity, I’d love to hear your opinions on this one. You can get a copy of the git repository using the following command:
$ git clone http://www.vector-seven.com/git/rails/plugins/form_collections.git
UPDATE: Here’s the contents of the README file:
> FormCollections
> ===============
>
> This plugin effectively rewrites UrlEncodedPairParser and provides the
> following features/changes:
>
> 1. The parameter parsing algorithm is much simpler
>
> There's all sorts of wacky stuff happening in the current implementation of
> UrlEncodedPairParser#parse. This plugin does away with that. A stack is no
> longer used during the parse. Parsing an Array vs. parsing a Hash is no longer
> all that different. See lib/patch_url_encoded_pair_parser.rb.
>
> 2. Parsing "a[b][0][c]=6" yields {"a" => {"b" => [{"c" => "6"}]}}
>
> Previously this would be parsed to: {"a" => {"b" => {"0" => {"c" => "6"}}}}
>
> In other words, parameters that were formerly treated as hashes with a numeric
> index are now actually arrays.
>
> This is important, because the order in which form fields are present may be
> important to the back-end processing code. Here, the array in "b" will
> preserve the ordering specified -- generating an Array rather than a Hash for
> numeric indices. Most other scenarios should essentially work as before, with
> the exception of a few error cases (e.g. parsing "a/b@[c][d[e][]=f" yields
> {"a/b@" => {"c" => {"d[e" => ["f"]}}} instead of {"a/b@" => {"c" => {}}}).
>
> The main problem here is that if an array is instantiated with a value at
> index 1000000, then we'll have 999999 nil elements. I figure this can easily
> be worked around with an Array-like structure that consumes only as much memory
> as it needs but otherwise acts as an Array, or a hard limit set via
> configuration variable.
>
> 3. fields_for now treats Arrays properly ...
>
> Example:
>
> <% fields_for @post.comments do |comment_form, comment| %>
> <%= comment.new_record? ? "" : comment_form.hidden_field(:id) %>
> <%= comment_form.hidden_field :post_id %>
> <%= comment_form.text_field :content %>
> <hr />
> <% end %>
>
> This will generate the following HTML for an array with two elements (one a new_record?, the other existing):
>
> <input id="comments_0_id" name="comments[0][id]" type="hidden" value="1" />
> <input id="comments_0_post_id" name="comments[0][post_id]" type="hidden" value="1" />
> <input id="comments_0_content" name="comments[0][content]" size="30" type="text" value="a test comment" />
> <hr />
> <input id="comments_1_id" name="comments[1][post_id]" type="hidden" value="1" />
> <input id="comments_1_content" name="comments[1][content]" size="30" type="text" value="" />
> <hr />
>
>
> Example
> =======
>
> Just install the plugin, and the new behavior should already be in effect.
>
> Copyright (c) 2008 Thomas Lee ..., released under the MIT license
>
So this new rails edge feature helps you?
http://ryandaigle.com/articles/2008/7/19/what-s-new-in-edge-rails-nested-models
Yep, Koz pointed this out on the mailing list. And yes, it’s dealing with part of the same problem. But the parsing itself is the big thing here:
Try building a form to generate :phone_numbers using edge. It’s not a matter of just using “user[phone_numbers][]” or “user[phone_numbers][0]“. The former won’t work because a “PhoneNumber” has two fields. The latter, because it builds a Hash instead of an Array. The existing form parsing code will limit the usefulness of this new capability.
I’m having trouble cloning the plug-in with git, and when I click the link to the repo, I receive a 403 error. Any suggestions?
I’ve been looking for a fix to this problem for months, and attempted writing my own hacked UrlEncodedPairParser with little success. I’ve tested this plugin in my application and it seems to work perfectly, even with multilevel nested arrays by adding an index option to the block params of the form helper.
Might need some hacking to work with Javascript added elements such as in Ryan’s complex forms recipe.
I would love to see this functionality in Rails 2.2
Eric: is it any better now? Migrated from lighttpd to Apache and I screwed up the configuration.
Ivan: Glad you found it useful.
It’s been a thorn in my side a few times in the past.
[...] UPDATE Since I wrote this post, I’ve written a plugin that replaces Rails’ parameter parsing implementation to make dealing with complex nested forms easier still. Refer to Taking the Pain Out of Complex Forms in Rails. [...]
Yep, was able to get it. Works great, too! Thanks