Dynamically Adding Items to a Select List

Problem

You want to add options to a select list efficiently, without requesting a full page with each added item. You've tried to add option elements by appending them to the DOM, but you get inconsistent results in different browsers when you flag the most recent addition as "selected." You also need the ability to re-sort the list as items are added.

Solution

First display the select list using a partial template that is passed an array of Tags. Next, use the form_remote_tag to submit a new tag for insertion into the database, and have the controller re-render the partial with an updated list of Tags.

Store tags in the database with the table defined by the following migration:

db/migrate/001_create_tags.rb:

class CreateTags < ActiveRecord::Migration
 def self.up
 create_table :tags do |t|
 t.column :name, :string
 t.column :created_on, :datetime
 end
 end
 def self.down
 drop_table :tags
 end end

You can require tag to be unique by using active record validation in the model:

app/models/tag.rb:

class Tag < ActiveRecord::Base
 validates_uniqueness_of :name end

In the layout, call javascript_include_tag :defaults, because you'll need both the functionality of the XMLHttpRequest object found in prototype.js as well as the visual effects of the script.aculo.us libraries.

app/views/layouts/tags.rhtml:

<html>
 <head>
 <title>Tags</title>
 <%= javascript_include_tag :defaults %>
 </head> 
 <body> 
 <%= yield %>
 </body> 
</html>

list.rhtml includes the new tag form, and a call to render :partial to display the list:

app/views/tags/list.rhtml:

<h1>Tags</h1>
<% form_remote_tag(:update => 'list', 
 :complete => visual_effect(:highlight, 'list'),
 :url => { :action => :add } ) do %>
 <%= text_field_tag :name %>
 <%= submit_tag "Add Tag" %>
<% end %>
<div id="list">
 <%= render :partial => "tags", :locals => {:tags => @tags} %>
</div>

The partial responsible for generating the select list contains:

app/views/tags/_tags.rhtml:

Total Tags: <b><%= tags.length %></b>;
<select name="tag" multiple="true" size="6">
 <% i = 1 %>
 <% for tag in tags %>
 <option value="<%= i %>"><%= tag.name %></option>
 <% i += 1 %> 
 <% end %>
</select>

The controller contains two actions: list, which passes a sorted list of tags for initial display, and add, which attempts to add new tags and re-renders the select list:

app/controllers/tags_controller.rb:

class TagsController < ApplicationController
 def list
 @tags = Tag.find(:all,:order => "created_on desc")
 end
 def add
 Tag.create(:name => params[:name])
 @tags = Tag.find(:all, :order => "created_on desc")
 render :partial => "tags", :locals => {:tags => @tags}, :layout => false
 end end

Discussion

The solution illustrates the flexibility of having controllers return prepared partials in response to Ajax requests. The view constructs a form that submits an Ajax request, calling the add action in the Tags controller. That action attempts to add the new tag and, in turn, re-renders the tag select list partial, with an updated list of Tags.

The responsiveness or flexibly gained with Ajax often comes at the cost of confusion: the user often doesn't get enough feedback about what is happening. The solution makes several attempts to make it obvious when a tag is added. It increments the tag total (which is displayed in the _tags partial); displays the new tag at the top of the multiselect list (which is ordered by creation time), where it can be easily seen without scrolling; and it uses the :complete callback (called when the XMLHttpRequest is complete) to momentarily highlight the new tag in yellow.

shows "Lisp" being added to the list.

Figure 8-3. A form that dynamically adds items to a select list