Saturday, 24 January 2009

The View Part 4 - Using Select in Forms

Last time around, I discussed forms, I am now going to focus on the select widget, as I found this particular mysterious at first. I am going to assume you have already read the page on forms.

Using f.select
The easiest way to use select is inside a FormBuilder block. While most FormBuilder code is in form_helper.rb, the select is in form_options_helper.

Suppose you have a column for integers in your database table (let us say "status", for example, in a table called "posts"). You want the user to be able to select an option from a drop-down list to set the value of the column. First, you need a set of options, and this is best defined in your model. This could be an array or a hash. If you use a hash, you can assign values to options yourself, but generally an array will be sufficient. I am going to use a hash, so in posts.rb, there will be this constant defined:
STATUS_OPTIONS = {'Read' => 1, 'Unread' => 2, 'Deleted' => 12}

Then, in the view, you just need a f.select. It might look something like this (with other fields removed for clarity):
<% form_for(@post) do |f| %>
<p>
Please select:
<%= f.select(:status, Post::STATUS_OPTIONS) %>
</p>
<p>
<%= f.submit "Update" %>
</p>
<% end %>

The f.select takes two parameters, the first being the name of the column (or any method as a symbol), the second is the array or hash. That is all you need to do. Rails will handle the saving and setting of the options for you.

Be aware that if you use an array, Rails will ignore the index. The value returned from the column method must be a value in the array, rather than a number for the index, and similarly what is set will be value, not the index. Personally, I found that annoying, so created a new method that would accept an array, and build a select element using the indices with the values.
# See actionpack/lib/action_view/helpers/form_options_helper.rb
module ActionView
module Helpers
class FormBuilder
def array_select(method, choices, options = {}, html_options = {})
h = {}
choices.each_index { |i| h.store(choices[i], i)}
@template.select(@object_name, method, h, objectify_options(options), @default_options.merge(html_options))
end
end
end
end


Using select_tag
The select_tag method is a bit poor, as it does not accept an array or hash, demanding instead a string with each entry surrounded by "<option>" and "</option>". Why it was not designed to accept an array and a value I cannot imagine. Instead, you have to use the options_for_select helper method, like this:
<%= select_tag("post[status]",
options_for_select(Post::STATUS_OPTIONS,
@post.status))
%>

However, there is also a select method that does the job.
<%= select(:post, :status, Post::STATUS_OPTIONS) %>

I imagine this was a later addition to Rails.

NOTE: I have read that select_tag should be used for GET commands, and select for POST (see here).

Submit on change to a select
Sometimes, you want the user to be able to select from a list, and to be taken straight to a new web page, without having to click on a button. This is pretty easy, with a bit of JavaScript. I set up a helper method to do that:
def submit_on_change
{:onchange => 'submit()'}
end

You can then add that method to your select or select_tag. Note that select takes two optional hashes, and you want to use the second, so I have put in an empty hash in that case.
<%= f.select :status, Post::STATUS_OPTIONS, {}, submit_on_change %>

<%= select_tag "post[status]",
options_for_select(Post::STATUS_OPTIONS,
@post.status),
submit_on_change %>


Another select example
Here is an example of using a select box to choose a web page. The web pages are set up in the model:
HELP_PAGES = {'Main' => 'index', 'Ruby Basics' => 'ruby', 'Ruby Classes' => 'classes'}

In the view, this code will set up the select (note that there is no submit button; be aware that any user with JavaScript disabled will not be able to navigate using this):
<% form_tag( {:action => :help, }, :method => :get) do %>
<%= select_tag "page", options_for_select(Post::HELP_PAGES),
submit_on_change %>
<% end %>

In the controller, the chosen page is handled:
def help
@page = params[:page]
# etc...
end


The collection_select method
Let us suppose you have one table associated with another, and want to be able to have the user select a record from one table for a record in the other. Let us go back to the archetypal blog application: Posts can be associated with a category (so a post belongs_to a category; a category has_many posts and the post table has a column called "category_id"). The user clicks on new post, writes his throughts, then can select from a list of categories from a drop-down list. How do we create such a thing?

This is what the collection_select method is for. As with select, there are two forms, one associated with a FormBuilder object, the other not.
<%= f.collection_select(:category_id,
Category.find(:all), :id, :name) %>
<%= collection_select(:post, :category_id,
Category.find(:all), :id, :name) %>

Note that the second form requires an extra parameter specifying the table we are modifying. The next parameter, :category_id in the example, is a method that is called to set the value; generally that will be the name of the column in the table you are modifying.

The next parameter is an array (kind of) of ActiveRecords; this is the list of options that will be available to the user. The next parameter, :id, is the method used by Rails to get values for each option of the select, while the next parameter determines the display name for the options. In effect, these two are the column names in the other table. To generate the list of options in the example, Rails iterates through the array of categories, and for each member it calls the "id" method to set the value, and the "name" method to set the text that is displayed.

As with select, there are two optional parameters for hashes of options.

As it turns out, you are not restricted to ActiveRecords. I tried it with this TestClass:
class TestClass
def initialize id, name
@id = id
@name = name
end
attr_reader :id, :name
end

Setting up an array:
  TEST_ARRAY = [
TestClass.new(12, 'First of all'),
TestClass.new(54, 'Middle'),
TestClass.new(32, 'Last and finally'),
]

And then using the collection_select like this:
<%= collection_select(:comment, :post_id,
Comment::TEST_ARRAY, :id, :name) %>

However, I have no idea why you would want to do that, rather than using a hash with select.

Selecting Dates
There are are set of methods to help you handle dates. If you are inside a FormBuilder block, just use date_select like this (Rails will even do this for you when you generate views):
<%= f.date_select :birthday %>
ActiveRecord will handle the rest. Outside FormBuilder you can use date_select or select_date (why not date_select_tag, which would be more consistent?). I found date_select easier to set up, but the values in the hash are not trivial to handle. I found some useful code here:

# Reconstruct a date object from date_select helper form params
def build_date_from_params(field_name, params)
Date.new(params["#{field_name.to_s}(1i)"].to_i,
params["#{field_name.to_s}(2i)"].to_i,
params["#{field_name.to_s}(3i)"].to_i)
end

date = build_date_from_params(:published_at, params[:article])


The API:
http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html
http://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html
http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html

See also:
http://shiningthrough.co.uk/Select+helper+methods+in+Ruby+on+Rails

Struggling with Ruby: Contents Page

No comments: