Peter Marklund

Peter Marklund's Home

Wed August 23, 2006
Programming

Rails Workaround: Preserving Nested Hash Params With List Values Across Requests

Suppose you have an HTML search form with a multi-valued select with the name "person[personal][interests][]". The trailing brackets are there to indicate to rails that this HTTP parameter should be converted to a Ruby list. Now, suppose the form submits to the search action in the contacts controller. Our params hash will then contain:

    params = {
      :controller => 'contacts',
      :action => 'search',
      :person => {
        :personal => {
          :interests => ['music', 'chess']
        }
      }
    }

So far so good. Now suppose you have pagination links on the search results page and also that you want to provide a link or button back to the search form for refining the search criteria. Here come the bad news - url_for which is used to create links doesn't support nested hash parameters and also doesn't support list values (there are some patches already to address at least the nesting problem). Here is what url_for produces in this case:

    > puts CGI.unescape(url_for(params))
    > /contacts/search/?person=personalinterestsmusicchess

Clearly url_for is inadequate in this case. I have written a set of helper methods to help save the day:

  def flatten_hash(hash = params, ancestor_names = [])
    flat_hash = {}
    hash.each do |k, v|
      names = Array.new(ancestor_names)
      names << k
      if v.is_a?(Hash)
        flat_hash.merge!(flatten_hash(v, names))
      else
        key = flat_hash_key(names)
        key += "[]" if v.is_a?(Array)
        flat_hash[key] = v
      end
    end
    
    flat_hash
  end
  
  def flat_hash_key(names)
    names = Array.new(names)
    name = names.shift.to_s.dup 
    names.each do |n|
      name << "[#{n}]"
    end
    name
  end
  
  def hash_as_hidden_fields(hash = params)
    hidden_fields = []
    flatten_hash(hash).each do |name, value|
      value = [value] if !value.is_a?(Array)
      value.each do |v|
        hidden_fields << hidden_field_tag(name, v.to_s, :id => nil)          
      end
    end
    
    hidden_fields.join("\n")
  end

Here is the output of my helpers for our example hash above:

  > puts CGI.unescape(url_for(flatten_hash(params)))
  > /contacts/search?person[personal][interests][][]=music&person[personal][interests][][]=chess

  > puts hash_as_hidden_fields(params)
  > <input name="action" type="hidden" value="search" />
  > <input name="controller" type="hidden" value="contacts" />
  > <input name="person[personal][interests][]" type="hidden" value="music" />
  > <input name="person[personal][interests][]" type="hidden" value="chess" />

Hope this will save other Rails programmers some time and frustration.

Comments

Derek Haynes said over 8 years ago:

Great work Peter! Very helpful.

-Derek

--------------------------------------------------------------------------------

Jonathan said over 8 years ago:

Peter…

Awesome….saved my butt!

Thank you!
Jonathan

--------------------------------------------------------------------------------

Josh Martin said over 7 years ago:

wish I saw this 3 hours ago

--------------------------------------------------------------------------------

Michael said over 7 years ago:

Currently seeing some odd behavior with this. It works some of the time…

If I have an array of values assigned to a hash, like

“foo[bar][]”=>[“1”, “2”, “3”]

Then CGI.escape(url_for(flatten_hash(params))) prints:

index?foo[bar][]=1/2/3

The nesting seems to work, but not the multiple selects. Otherwise, everything is perfect.

--------------------------------------------------------------------------------

Vanja said over 7 years ago:

Thank you for the solution!
It took the better part od a week to try to hack into Rails but this is a much happier solution – at least until the Rails make it too :-)
Thanks again!

--------------------------------------------------------------------------------

Georg Ledermann said over 5 years ago:

Nice code, thank you very much. I wonder if it's needed in current Rails 2.3.4. Is it?

--------------------------------------------------------------------------------