LRBlog

Logical Reality Design: Web Design and Software Development

Getting <select> options in the right order

June 23, 2008

Sometimes you may want to generate a selector with a fixed set of options.   In one recent task of mine, we needed a selector for an integer 1 through 10, but the client wanted them labeled also with text to identify how the numbers mapped to the words “High” “Medium” and “Low”. (We were selecting the priority level of a task in a project management application). Essentially, I want to generate this HTML output:


<select id="task_priority" name="task[priority]"> 
    <option value="1">1 - High</option> 
    <option value="2">2 - High</option> 
    <option value="3">3 - High</option> 
    <option value="4">4 - Med</option> 
    <option selected="selected" value="5">5 - Med</option> 
    <option value="6">6 - Med</option> 
    <option value="7">7 - Med</option> 
    <option value="8">8 - Low</option> 
    <option value="9">9 - Low</option> 
    <option value="10">10 - Low</option>
</select>

These are fixed name/value pairs, so it made sense to me to store them as a constant hash in my Task model:

Hash in Task.rb


PRIORITY_OPTIONS = { "1 - High" =&gt; "1", "2 - High" =&gt; "2", "3 - High" =&gt; "3",
"4 - Med" =&gt; "4", "5 - Med" =&gt; "5", "6 - Med" =&gt; "6", "7 - Med" =&gt; "7",
"8 - Low" =&gt; "8", "9 - Low" =&gt; "9", "10 - Low" =&gt; "10"  }

However, I was rather dismayed to discover that this didn’t produce the results I wanted, because Ruby hashes (in Ruby 1.8.6) do not preserve order. This is what came out:

Badly ordered priority selector

That’s not what I wanted! I knew that select will also take arrays, but of course I needed separate name/value pairs, which I can’t get with just straight arrays. I spent a while playing around with OrderedHash, which exists in Rails but is essentially undocumented and, as it turns out, does not support any useful functions of Hash like merge! and insert! that might make it easy to construct my list of options.

The Fix: Array of Arrays, or Array of Hashes

The documentation is not entirely clear on this, but if you send an array of 2-element arrays to select, rails will use the two elements of each inner array as if they were key and value pairs, and because the entire structure is an array it will preserve order. So, to get the results I wanted, I just need to change my constant to this:

PRIORITY_OPTIONS = [ ["1 - High", 1], ["2 - High", 2], ["3 - High", 3],
["4 - Med", 4], ["5 - Med", 5], ["6 - Med", 6], ["7 - Med", 7],
["8 - Low", 8], ["9 - Low", 9], ["10 - Low", 10] ]

And I get the result I was looking for :

Correctly ordered select options.

As it turns out, the way ActionView processes the options is fairly general: if you pass it any enumerable object, it will iterate that object, and for each element will check to see if that element supports the methods :first and :last (and isn’t a string). If so, it will generate an option with the text set to element.first and the value set to element.last. If it was a string, or didn’t support first and last, both the text and value of the option are set to the element itself.

Testing it

Here’s a handy function I use for testing it the presence of a selector. You pass it the name of your selector, the name of hash or array of options (in the any format supported by select), and optionally the value of an item that should be pre-selected, and it will assert the existence of each of those things. If you need to handle selectors with multiple selections, you can just wrap the last assertion in a loop.

Drop this in test/test_helper.rb

  # Assert existence of form select input
  # the <option> tags can be passed either as 
  # a hash like this: {'foo'=>'bar', 'baz'=>'qux'} 
  # or an array of arrays like this: [ ['foo', 'bar'], ['baz', 'qux'] ]  
  # either will be asserted to match:
  #         <option value='bar'>foo</option>
  #         <option value='qux'>baz</option>
  # or as an array like this:
  #   [ 'foo', 'bar' ] will be asserted to match
  #         <option value='foo'>foo</option>
  #         <option value='bar'>bar</option>
  def assert_select_input(name, option_values, options={})
    attributes = { :name => name }

    # first assert that the select tag exists
    selector = { :tag => "select", :attributes => attributes }
    assert_tag selector   
        
    option_values.each do | opt |
      if !opt.is_a?(String) and opt.respond_to?(:first) and opt.respond_to?(:last)
        assert_tag({:tag => "option", :attributes => { :value => opt.last },      
          :parent => selector, :content => opt.first })
      else
        assert_tag({:tag => "option", :attributes => { :value => opt },      
          :parent => selector, :content => opt })               
      end
    end        
    
    # check for the pre-selected option, if any
    if options[:selected] 
      assert_tag :tag => "option", :attributes => {:selected => 'selected', 
        :value => options[:selected] }
    end    
  end  

As an example of how to use it, here’s my method for testing the task priority selector pictured above:

  # asserts a <SELECT> field, targeting task[priority], containing ten
  # appropriately formatted <OPTIONS>, and with selected_num (if any) the
  # pre-selected option
  def assert_priority_select(selected_num = nil)
    options = selected_num ? { :selected => selected_num } : Hash.new
    assert_select_input "task[priority]", Task::PRIORITY_OPTIONS, options  
  end

Where the array specified in the Task model looks like this:

  PRIORITY_OPTIONS = [ ["1 - High", 1], ["2 - High", 2], ["3 - High", 3], 
    ["4 - Med", 4], ["5 - Med", 5], ["6 - Med", 6], ["7 - Med", 7],
    ["8 - Low", 8], ["9 - Low", 9], ["10 - Low", 10] ]

Add A Comment