I’ve always thought generating select tags was a bit odd in Rails. There are various choices and it might be difficult to decide which to use in a specific situation. A popular article on the topic is this one: Select helper methods in Ruby on Rails. It’s pretty old (2007), but still relevant. I’ll go through the helpers in that post quickly:
collection_select: Mostly used for model-backed data, invoked with all the method names it needs to build up the select box.select_tag: A lot simpler, requires the option tags as a string, which usually needs to be delegated to another helper.select: Used with a hash of names and values or with a list of pairs. This means that you can use it for any kind of data, including one from a model, but you need to prepare it first.
What the article doesn’t cover, though, is the grouped select helpers. They’re
used when you need to categorize the data with optgroup tags. There’s info on
the Net here and there, but I’ll try to give a quick run-down on how and when
to use them. I’ll be using the FormBuilder variants of the helpers, but I’ll
also give an example later on for non-resource forms.
Grouping collections and their children
To create a selection box from a collection, you can use
grouped_collection_select. As you might have guessed, it’s very similar to
collection_select, except it receives a few additional parameters. I’ll
borrow an example straight from the
rails docs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Now, if we want to display a form to set a city’s country, and group the options by continent, we could do this:
1 2 3 4 5 6 | |
As you can see, it’s a pretty long method call, but there’s nothing really complicated going on:
:country_idis the field we’re assigning to@continentsis the parent collection:countriesis the method we’re calling on each continent to retrieve the records for the<option>tags:nameis the method that will be used for displaying each continent:idand:nameare the key and value methods for each country
I’m quite sure I’ll never be able to use this method without looking it up, but
it does a good job for a simple case. A close relative,
option_groups_from_collection_for_select might be a bit more useful in
practice, but more on that a bit later.
More generic select tags with select_tag
Drop-downs are pretty useful for displaying various choices on the user
interface. The grouped_collection_select helper is meant to be used
specifically with model properties, so it might not be immediately obvious how
to create generic selects. The helper in this case is select_tag:
1 2 | |
As noted at the start of the post, this one is the simplest one of the bunch. Its second argument is just a string containing all option tags as HTML. If we want to get a grouped select, we need to change the structure and the helper we’re using:
1 2 3 4 5 | |
You can also use options_from_collection_for_select or
option_groups_from_collection_for_select to generate option tags in the same
way as the collection select helpers. It gets a bit long-winded, but I really
can’t think of shorter names myself. The important thing is that this set of
helpers has the same feature set as the FormBuilder ones, albeit with a
slightly different API.
Note: A small inconsistency
Ordinarily, when using a resource form with form_for, the helper methods have
the same names as standalone form helpers, except the suffix “_tag” is removed.
For example, these two forms are mostly equivalent:
1 2 3 4 5 6 7 8 9 10 11 | |
So, the usual convention is that standalone helpers have similar FormBuilder
ones, whose names lack the “_tag” suffix. The select helpers break that
pattern: select_tag doesn’t have a FormBuilder equivalent. Instead,
FormBuilder#select mimics the behavior of the select helper:
1 2 3 4 | |
It’s a bit surprising, but I think that’s the only helper that doesn’t follow the convention, so it’s just something to take note of.
Grouping models by characteristics
While we can use grouped_collection_select for model relationships, this
doesn’t help us if we want to group models by the value of some attribute.
Let’s take this for an example:
1 2 3 4 5 6 7 8 9 | |
We want to display a selection box for a post’s category and group the available ones based on whether they’re active or not. First of all, let’s generate the grouping as a data structure:
1 2 3 4 5 6 7 8 | |
It’s not the most efficient way to do this, but it should be easy to understand. Basically, the keys are the labels to display in the optgroups and the values are collections of key/value pairs to use for the option tags. Obviously, you could put this code anywhere you like. To me, it seems sensible to keep it tucked in the model, since it’s just a data structure, but you might consider it specific enough to be put in a helper in the view layer, for example.
Now, to get the actual select box, you could use select_tag with the
appropriate option helper:
1 2 3 | |
A very nice bonus with this approach is that the grouping can easily be changed
by modifying the behavior of Post::for_select. If you’d like the groups to be
called “enabled” and “disabled” instead, you just have to modify that method
instead of hunting it down in the view layer. You can even remove grouping
altogether and use Post.all instead, although that would require changing the
option helper.
Unfortunately, select_tag is great for the general case, but not very well
suited for model forms. You need to specify the name attribute yourself, as
post[category_id], which might be a problem if you decide to rename your
model or use inheritance. It would be much nicer if we could use something like
the select helper. The problem is, you currently can’t – select only works
with flat collections and there’s no such thing as a grouped_select. However,
interestingly enough, you can use select with a string:
1 2 3 | |
The grouped_options_for_select helper generates the option tags in a string,
and select simply uses it as it is. This doesn’t seem to be a documented
feature, possibly because it looks like a side effect of delegating to other
helpers – the relevant source is
here.
Still, it doesn’t look like a feature that’s likely to change anytime soon.
This method can also replace grouped_collection_select. Using the example
with the cities, countries and continents, we could define the data like so:
1 2 3 4 5 6 7 | |
Note that it works not only with hashes, but also with lists of pairs, where the first item is the key and the second is the collection for the options.
The form is almost exactly the same as with the previous select example:
1 2 3 | |
A drawback in this case is that it might get a bit complicated to add custom
logic to the Country::for_select method. While it’s true that
grouped_collection_select requires a lot of arguments, that lets you isolate
the logic in scopes and might be a better choice sometimes.
Summary
collection_selectandgrouped_collection_selectare meant to be used when dealing with model data. The invocation gets long, but their many arguments make them pretty customizable.- When you need a select tag that is not linked to a model attribute, you can
do it with
select_tagand choose a helper method to generate your option tags. - Most
FormBuilderhelpers have standalone versions that end in “_tag”, butFormBuilder#selectisnotequivalent toselect_tag. - You can use
selectfor arbitrary collections by relying on one of the option-generating helpers. It requires some more work to prepare the data structure, but this lets you customize it with only a few changes to the view layer.