RESTful Dynamic Select Lists in Rails

It is often the case that you want to have a form with multiple select lists where one is dependent upon the selection of another. In Rails parlance, maybe you want to select a parent record in one select field, then populate another select field with all the children records from the has_many association.

There are many ways to do this, but let's look at implementing this in a RESTful manner, using a custom response type to control what type of data we get from our Controller. This helps keep our controllers DRY and our views happy. We use AJAX, of course, so make sure to enable the Javascript defaults in your application layout. This is JS framework agnostic, so you can use either JQuery or Prototype (assuming you have either JRails installed for Rails 2.3, or are using the proper rails.js adapter in Rails 3.0).

Let's say that we have a Client model that has many Projects:


# app/models/client.rb
class Client < ActiveRecord::Base

has_many :projects

end

# app/models/project.rb
class Project < ActiveRecord::Base

belongs_to :client

end


Now let's say we have a Tickets model that records time spent working on a project. When we create a new ticket, we want to select a project, but we don't want to display every single project for every client in our database. We'd like to select the client first, then see a select field get populated with projects for that client.

The first thing we do is make sure we have a projects controller that will return a collection of Project records. We add a little bit of logic to scope it by a Client if the controller action receives a client_id as a parameter:


class ProjectsController < ApplicationController
def index
@projects = params[:client_id].present? ? Client.find(params[:client_id]).projects.all : Project.all
respond_to do |format|
format.html
format.xml
end
end
end


Now, in order to keep this controller RESTful, we don't want to add an additional action to return data only for our select list, and we don't want to override the default HTML or JS responses, since we might want to use those for managing Projects.

The trick is to create a custom MIME type:


# config/initializers/mime_types.rb

Mime::Type.register_alias "text/html", :for_select


Now, in our ProjectsController, update the respond_to block:


class ProjectsController < ApplicationController
def index
@projects = params[:client_id].present? ? Client.find(params[:client_id]).projects.all : Project.all
respond_to do |format|
format.html
format.for_select { render :layout => false }
format.xml
end
end
end


We tell the index action to respond to a custom MIME typeformat, for_select, which we tell Rails is just another way of delivering HTML content. But we can use the format type to tell Rails to respond to that request by rendering the following view that we create in the app/views/projects directory:


# app/views/projects/index.for_select.haml
= options_from_collection_for_select @projects, :id, :name


This view just renders a set of <option> tags. Notice we turned off the layout rendering in the respond_to block.

Our actual form that utilizers this would be a form for creating a new ticket, and would look something like this:


= form_for @ticket do |f|
%p
= f.label :client_id
%br
= f.collection_select :client_id, Client.all, :id, :name

%p
= f.label :project_id
%br
= f.select :project_id

= observe_field :client_id, :url => projects_path(:format => "for_select"), :method => :get, :update => :ticket_project_id, :with => "'client_id=' + value"


The observe_field helper will watch the client select field for any new selection by the user, then fire off a GET request to /projects/index.for_select?client_id=x, where x is the current value of the Client ID in the select list. We tell the observer to update the contents of the the DOM element #ticket_project_id (which is the ID generated for the project_id select list in the context of the Ticket form) with the response we get from the projects controller, which in our case is nothing but a set of option tags of our projects.

Violá, you have AJAXy, dynamically-set Select lists without writing a line of JS.

To recap, the only real trick here was to take advantage of Rails' respond_to block with a custom MIME-type. Using MIME type aliases, we can create arbitrary formats that have special behavior within controllers for situations like these. It keeps the main response types clean, and we stay flexible, agile, and all that other good stuff.

THE END (?)