I want to do the following in Rails. Anyone know if it’s possible?
I have a a sortable list of pictures with a bunch of embedded form elements (thumbnail and full-size URLs, remote IDs, etc.). I’d like to set this up with array-style form elements so that the order of the form elements in the DOM would itself tell the server the order of the pictures, without my having to constantly recalculate the sort order and store it in a dedicated input. In other words, the output should look like this, if the overall param were named images:
"images" => [{"thumb" => url, "full" => url, "detail" => "data"}, {"thumb" => url, "full" => url, "detail" => "data"}, {"thumb" => url, "full" => url, "detail" => "data"}, ...]
I’ve tried a number of different approaches, but none seem to work. Most turn the form into an array with a single hash element, which doesn’t work because the hash doesn’t preserve the key order. Here’s a sampling of the formats I used and the results they gave:
<input type="hidden" name="images[][bar]" value="2">
# "images"=>[{"bam"=>"2", "baz"=>"2", "bar"=>"2"}]
<input type="hidden" name="images[][bar][a]" value="2">
# "images"=>[{"bam"=>{"a"=>"2", "b"=>"2"}, "baz"=>{"a"=>"2", "b"=>"2"}, "bar"=>{"a"=>"2", "b"=>"2"}}]
<input type="hidden" name="images[][a][bar][][a]" value="2">
# "images"=>[{"a"=>{"bam"=>[{"a"=>"2", "b"=>"2"}], "baz"=>[{"a"=>"2", "b"=>"2"}], "bar"=>[{"a"=>"2", "b"=>"2"}]}}]
<input type="hidden" name="images[][][bar][][a]" value="2">
# "images"=>[{"bam"=>[{"a"=>"2", "b"=>"2"}], "baz"=>[{"a"=>"2", "b"=>"2"}], "bar"=>[{"a"=>"2", "b"=>"2"}]}]
<input type="hidden" name="images[][bam]" value="2">
<input type="hidden" name="images[][bar][a]" value="2">
<input type="hidden" name="images[][bar][b]" value="2">
# malformed result!
# /!\ FAILSAFE /!\ Sun Nov 21 11:05:44 +0100 2010
# Status: 500 Internal Server Error
# can't convert nil into Hash
<input type="hidden" name="images[][bam][]" value="2">
<input type="hidden" name="images[][bar][a]" value="2">
<input type="hidden" name="images[][bar][b]" value="2">
# ditto
I traced the error I generated with the two last examples back and found myself looking at the normalize_params method in Rack’s utils.rb (this code is the same in both Rack 1.0.1 and 1.2.1). This is where Rails/Rack parses the form data and turns them into our familiar params hash. Ignoring the error, I took a look at how the parsing is done:
def normalize_params(params, name, v = nil)
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
k = $1 || ''
after = $' || ''
return if k.empty?
if after == ""
params[k] = v
elsif after == "[]"
params[k] ||= []
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
params[k] << v
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
child_key = $1
params[k] ||= []
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
normalize_params(params[k].last, child_key, v)
else
params[k] << normalize_params({}, child_key, v)
end
else
params[k] ||= {}
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
params[k] = normalize_params(params[k], after, v)
end
return params
end
I’m not a regular expressions master, but if I understand this right, what I want to do doesn’t seem possible.
Assuming so, it seems the best solution seems to be to give each image (represented as a param hash) an order parameter; when the user moves elements around, I’d crawl each order parameter and update the order value so it can be stored appropriately by the server. It’s less elegant, but it’ll work.
I’d be very grateful for any opinions/experience — is my original design indeed impossible?
Alex
Comments
My first instinct is to flip it around, have input keys like “images[attr1][]“, “images[attr2][]” and so on, and then flip it in ruby from a hash of ordered lists of attributes to an ordered list of hashes of attributes (if necessary).
I did a cursory test and it works in Sinatra, but didn’t get a chance to test in Rails – the option parsing may be different enough that it’s not an option.
I’ll check when I get back home, but I gotta run!
Nevermind… Sinatra I guess is smarter than Rails, because I can use this HAML (I imagine HTML would get horribly violated by the comment form, so I’m not even gonna try):
%form{ :method => “POST” }
%input{ :name => “images[][thumb]“, :value => “thumb1″ }
%input{ :name => “images[][full]“, :value => “full1″ }
%input{ :name => “images[][data]“, :value => “data1″ }
%br
%input{ :name => “images[][thumb]“, :value => “thumb2″ }
%input{ :name => “images[][full]“, :value => “full2″ }
%input{ :name => “images[][data]“, :value => “data2″ }
%br
%input{ :name => “images[][thumb]“, :value => “thumb3″ }
%input{ :name => “images[][full]“, :value => “full3″ }
%input{ :name => “images[][data]“, :value => “data3″ }
%br
%input{ :type => :submit }
To get these POST params:
{“images”=>[{"thumb"=>"thumb1", "full"=>"full1", "data"=>"data1"}, {"thumb"=>"thumb2", "full"=>"full2", "data"=>"data2"}, {"thumb"=>"thumb3", "full"=>"full3", "data"=>"data3"}]}
That’s what you want, right?
I should point out that if there is ever even the slightest chance of non-homogenous field names, both approaches should break down. The safer way to do it here would probably be something like giving each image a unique ID (large-ish random is probably good enough here), and then doing
hidden imageorder[] = RANDOMID1
input image[RANDOMID1][thumb]
input image[RANDOMID1][full]
hidden imageorder[] = RANDOMID2
input image[RANDOMID2][thumb]
and so on…
That way, if the DOM elements are re-ordered, the order of RANDOMIDs will change accordingly, but you essentially only have one place that the order is tracked, rather than sort of tangling it up in the way the rest of the data is encoded. It’s less error-prone, IMNSHO.
Hey Yitz,
Thanks for the research! It’s odd that Rails and Sinatra differ, since the parameter parsing seems to happen in the Rack middleware.
After giving it some thought, though, I agree with you that such a construction is too brittle to be useful. These inputs are being constructed and used by a Javascript image uploading library I’m writing (http://github.com/arsduo/rainydays) and since it’s open source I don’t want it to depend on a particular server-side implementation of parameter parsing.
The second approach you recommend — generating a random ID to serve as the hash key and including an order value — is the way I’m going to take development. I have to give some thought to where to store the order. In your model you put it as a separate field entirely, which has the huge advantage of not needing any additional work to keep it organized. That’s a big enough advantage that I’ll probably go with it.
The alternative I’d been thinking about was to store a position value in each hash (e.g. image[RANDOMID][position]). Sorting that in Rails would be easier since you don’t have to depend on either the external parameter or the random ID, which you could discard entirely:
params[:images].keys.sort {|details1, details2| details1["order"] <=> details2["position"]}Unfortunately, you’d need additional logic on the client side to update the position each time the image order is changed. That’s easy to implement, but it’s still additional logic and hence less elegant.
I’ll give it a spin this week and see how the implementation goes.
Alex
my God, i believed you were going to chip in with some decisive insght at the end there, not leave it
with ‘we leave it to you to decide’.