125

I have a form select statement, like this:

= f.select :country_id, @countries.map{ |c| [c.name, c.id] }

Which results in this code:

...
<option value="1">Andorra</option>
<option value="2">Argentina</option>
...

But I want to add a custom HTML attribute to my options, like this:

...
<option value="1" currency_code="XXX">Andorra</option>
<option value="2" currency_code="YYY">Argentina</option>
...
Daniel Rikowski
  • 71,375
  • 57
  • 251
  • 329
el_quick
  • 4,656
  • 11
  • 45
  • 53
  • 2
    Rails doesn't provide that functionality, you'll have to create a helper to create that markup. Also, bear in mind that the example you mentioned is not valid HTML. – Augusto Feb 19 '11 at 19:26
  • I know, my example is not valid html... I guess I have to change my way to get the results that I want, thk! – el_quick Feb 19 '11 at 19:33
  • To current seekers, Rails does provide the functionality (looks like as of Rails 3?) Check current docs on `options_for_select`: https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-options_for_select – Danny Apr 26 '23 at 20:07

5 Answers5

372

Rails CAN add custom attributes to select options, using the existing options_for_select helper. You almost had it right in the code in your question. Using html5 data-attributes:

<%= f.select :country_id, options_for_select(
    @countries.map{ |c| [c.name, c.id, {'data-currency_code'=>c.currency_code}] }) %>

Adding an initial selection:

<%= f.select :country_id, options_for_select(
    @countries.map{ |c| [c.name, c.id, {'data-currency_code'=>c.currency_code}] }, 
    selected_key = f.object.country_id) %>

If you need grouped options, you can use the grouped_options_for_select helper, like this (if @continents is an array of continent objects, each having a countries method):

<%= f.select :country_id, grouped_options_for_select(
    @continents.map{ |group| [group.name, group.countries.
    map{ |c| [c.name, c.id, {'data-currency_code'=>c.currency_code}] } ] }, 
    selected_key = f.object.country_id) %>

Credit should go to paul @ pogodan who posted about finding this not in the docs, but by reading the rails source. https://web.archive.org/web/20130128223827/http://www.pogodan.com/blog/2011/02/24/custom-html-attributes-in-options-for-select

Anatortoise House
  • 4,941
  • 1
  • 22
  • 18
6

You could do this as follows:

= f.select :country_id, @countries.map{ |c| [c.name, c.id, { 'data-currency-code' => c.currency_code} ] }
Nikhil Gupte
  • 3,266
  • 4
  • 25
  • 15
  • Correct, but already mentioned in Anatortoise House's answer. – Kelvin May 31 '13 at 21:35
  • The accepted answer doesn't illustrate the custom attributes using ruby. This one does hence I feels it's better as it's the first answer that shows how to do it via ruby. – Nikhil Gupte Jun 02 '13 at 02:49
5

This is not possible directly with Rails, and you'll have to create your own helper to create the custom attributes. That said, there are probably two different ways to accomplish what you want:

(1) Using a custom attribute name in HTML5. In HTML5 you are allowed to have custom attribute names, but they have to be pre-pended with 'data-'. These custom attributes will not get submitted with your form, but they can be used to access your elements in Javascript. If you want to accomplish this, I would recommend creating a helper that generates options like this:

<option value="1" data-currecy-code="XXX">Andorra</option>

(2) Using values with custom splitting to submit additional data. If you actually want to submit the currency-code, I would recommend creating your select box like this:

= f.select :country_id, @countries.map{ |c| [c.name, "#{c.id}:#{c.currency_code}"] }

This should generate HTML that looks like this:

<option value="1:XXX">Andorra</option>
<option value="2:YYY">Argentina</option>

Which you can then parse in your controller:

@id, @currency_code = params[:country_id].split(':')
Pan Thomakos
  • 34,082
  • 9
  • 88
  • 85
4

The extra attributes hash is only supported in Rails 3.

If you're on Rails 2.x, and want to override options_for_select

I basically just copied the Rails 3 code. You need to override these 3 methods:

def options_for_select(container, selected = nil)
    return container if String === container
    container = container.to_a if Hash === container
    selected, disabled = extract_selected_and_disabled(selected)

    options_for_select = container.inject([]) do |options, element|
      html_attributes = option_html_attributes(element)
      text, value = option_text_and_value(element)
      selected_attribute = ' selected="selected"' if option_value_selected?(value, selected)
      disabled_attribute = ' disabled="disabled"' if disabled && option_value_selected?(value, disabled)
      options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}#{disabled_attribute}#{html_attributes}>#{html_escape(text.to_s)}</option>)
    end

    options_for_select.join("\n").html_safe
end

def option_text_and_value(option)
  # Options are [text, value] pairs or strings used for both.
  case
  when Array === option
    option = option.reject { |e| Hash === e }
    [option.first, option.last]
  when !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last)
    [option.first, option.last]
  else
    [option, option]
  end
end

def option_html_attributes(element)
  return "" unless Array === element
  html_attributes = []
  element.select { |e| Hash === e }.reduce({}, :merge).each do |k, v|
    html_attributes << " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\""
  end
  html_attributes.join
end

Kinda messy but it's an option. I place this code in a helper module called RailsOverrides which I then include in ApplicationHelper. You can also do a plugin/gem if you prefer.

One gotcha is that to take advantage of these methods you must always invoke options_for_select directly. Shortcuts like

select("post", "person_id", Person.all.collect {|p| [ p.name, p.id, {"data-stuff"=>"html5"} ] })

will yield the old results. Instead it should be:

select("post", "person_id", options_for_select(Person.all.collect {|p| [ p.name, p.id, {"data-stuff"=>"html5"} ] }))

Again not a great solution, but it might be worth it to get to the ever so useful data-attribute.

mastaBlasta
  • 5,700
  • 1
  • 24
  • 26
0

I ran into this issue as well and created the "enhanced_select" Ruby Gem to solve this problem. You can find it here:

https://github.com/bkuhlmann/enhanced_select