Autocomplete text input using unobtrusive jQuery in Ruby on Rails

Posted on 16 August 2010

Recently I needed to implement an autocomplete feature into a Ruby on Rails application so I thought I'd throw up the bare-bones of what I came up with as a simple example.

Note that it written unobtrusively using jQuery, but as it was for an admin system there is no non-javascript fall back.

So for my example we have groups and in those groups we have multiple users (joined by a group_users table). I want to be able to create a group and add all the users on one screen, using autocomplete to find the users.

The models...

class Group < ActiveRecord::Base
  has_many :group_users, :dependent => :destroy
  has_many :users, :through => :group_users
end

class GroupUser < ActiveRecord::Base
  belongs_to :group
  belongs_to :user
end

class User < ActiveRecord::Base
  has_many :group_users
  has_many :groups, :through => :group_users
  
  named_scope :autocomplete_name, lambda{ |name| {:include => :user, :conditions => ["users.name LIKE ?", "#{name}%"]} }
end

The groups controller will be restful with just a couple of extra instance variables to load up the users. I'm only going to include the creation methods here...

class GroupsController < ApplicationController

  def new
    @group = Group.new
    @users = @group.users
    respond_to do |format|
      format.html 
      format.xml  { render :xml => @group }
    end
  end

  def create
    @group = Group.new(params[:group])
    @users = @group.users
    respond_to do |format|
      if @group.save
        flash[:notice] = 'Group was successfully created.'
        format.html { redirect_to(@group) }
        format.xml  { render :xml => @group, :status => :created, :location => @group }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @group.errors, :status => :unprocessable_entity }
      end
    end
  end
  
end

In the users controller setup a couple of custom actions that will be called via ajax.

class UsersController < ApplicationController

  def load_user
    render :partial => "/users/details", :locals => {:user => User.find(params[:id])}
  end

  def autocomplete
    render :json => User.autocomplete_name(params[:term]).collect{ |user| {:value => user.id, :label => "#{user.name}"} }
  end
end

load_user is called once you have selected the user you want from the autocomplete suggestions. autocomplete does the searching for users based on the text the user enters. The json returned is digested by the javascript.

Routes will be as per normal except you'll need to update the users like so...

ActionController::Routing::Routes.draw do |map|
  map.resources :users, :collection => {:load_user => :get, :autocomplete => :get}
end

Now in groups/new .erb file you'll need...

<h1>New group</h1>

<%= error_messages_for :group %>
<% form_for(@group) do |f| %>
  
  <%= f.label :name, "Group name" %>
  <%= f.text_field :name %>

  <div>
    <label for="user_name">Add new user</label>
    <%= text_field_tag "user_name", nil, :class => "text", :id => "user_name" %>
  </div>

  <fieldset class="data_container">
    <% users.each do |user| %>
      <%= render "/users/details", :user => user %>
    <% end %>
  </fieldset>
  
  <%= submit_tag %>
  
<% end %>

So the gorup has a name attribute and below that is the autocompelte text input for searching for users. Below that we render out all the user details attached to the group, in reality i've split these fields into a partial where they are re-used in the edit action.

The users/details partial looks like this...

<div class="user_details">
  <h2><%= user.name %></h2>
  <dl>
    <dt>Address</dt>
    <dd><%= user.address %></dd>
    <dt>Telephone</dt>
    <dd><%= user.telephone %></dd>
  </dl>
  <%= hidden_field_tag "group[user_ids][]", user.id %>
  <%= content_tag(:p, link_to("Remove user", "#", :class => "remove_user")) %>
</div>

This partial is rendered whenever you select a user. The user attributes you output are not important, the key is the hidden field containing the user id. By passing this through in the form rails will automagically create all the group_member join records required to link the users to the group. No additional controller code is required.

In your javascript file add the following. Note that you will need jQuery UI 1.8

$(function() {
  $("#new_group, #edit_group").autocompleteUserName();  
});

$.fn.autocompleteUserName = function(){
  return this.each(function(){
    var input = $("#user_name", this);
    var dataContainer = $('.data_container',this);
    
    var loadData = function(item){
      if(item){
        var user_id = item.value;
        $.get("/users/load_user", {id:user_id}, function(data){
          if(data){ dataContainer.html(data); }
        });
      }
    }
    
    input.initAutocomplete(loadData, "/users/autocomplete");
    
    // remove links
    dataContainer.delegate('.remove_user','click',function(){
      $(this).closest('.user_details').remove();
      return false;
    });
  });
};


$.fn.initAutocomplete = function(callback, source){
  return this.each(function(){
    var input = $(this);
    input.autocomplete({
      source: source,
      minLength: 2, //user must type at least 2 characters
      select: function(event, ui) {
        if(ui.item){ 
          input.val(ui.item.label); 
          callback(ui.item);
        }
        return false;
      },
      focus: function(event, ui) { // triggered by keyboard selection
        if(ui.item){ input.val(ui.item.label); }
        return false;
      },
      change: function(event, ui) { // called after the menu closes
        if(callback){ input.val(""); }
      } 
    });
  });
}

So the autocompleteUserName function takes the user search string, sets up an ajax callback called loadData that will load the html onto the page and then calls the initAutocomplete function. This function uses the autocomplete functionality built into jQuery. Take a look at the jquery api to see how the autocomplete box behaviour can be tweaked.

I've split initAutocomplete out into its' own function so that I can have multiple autocomplete boxes that load different data.

I've also included a small amount of code in the autocompleteUserName function to allow for the removal of users from the group.

Below are some CSS styles are taken from the jQuery api and tweaked so they should provide a decent starting point...

.ui-autocomplete { position: absolute; cursor: default; }	
* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
.ui-menu {
	list-style:none;
	padding: 0px;
	margin: 0;
	display:block;
	float: left;
	border:1px solid #494849;
}
.ui-menu .ui-menu-item {
	margin:0;
	padding: 0;
	zoom: 1;
	float: left;
	clear: left;
	width: 100%;
	text-align:left;
}
.ui-menu .ui-menu-item a {
	text-decoration:none;
	display:block;
	line-height:1.4;
	zoom:1;
	background:#FFF;
  padding:5px;
}
.ui-menu .ui-menu-item a.ui-state-hover,
.ui-menu .ui-menu-item a.ui-state-active {
	font-weight: normal;
	background:#717171;
  color:#FFF;
  text-decoration:none;
}

Hope this helps someone out there!

About Paul

Paul works for Kyan web design agency in Surrey, UK as a Ruby on Rails developer.

Follow Paul on Twitter

Email: paulsturgess [at] gmail.com

Read more articles in the archive →

Comments...

  • Man, that's great! I mixed both your solution and the one described in JQuery UI documentation and worked perfectly. Thanks!

    André at 04 Oct 10 at 16:33

  • Do you have a public demo of this code's functionality anywhere? I'd like to play with it and see how it works.

    Thanks for the contribution!

    -Charlie

    Charlie at 18 Nov 10 at 16:04

  • hi this is murali i'm graphic web designer,
    one design issue for this page, add comment button height please increse to 2em

    thanks

    murali at 14 Feb 11 at 20:37

  • Hi Paul,

    Thanks for your explanation, it worked perfect though I got to do some minor changes to use it Rails 3. In order to work in rails 3,

    I changed the routes, this two lines were added before the resources :users declaration :

    match '/users/load_user', :to => 'users#load_user', :as => 'load_user'
    match '/users/autocomplete', :to => 'users#autocomplete', :as => 'autocomplete'

    And the users controller because name_scope is deprecated in rails 3 to:

    scope :autocomplete_name, lambda{ |name| {:conditions => ["customer.name LIKE ?", "#{name}%"]} }

    Thanks again, Jimmy

    Jimmy at 21 Feb 11 at 16:15

  • Thanks Paul! This just might work for a restaurant menu side project.

    CaptProton at 25 Apr 11 at 17:23

  • i am beginner to ruby on rails can you send the complete project to my email <h1>praneetheee240@gmail.com</h1>. i am using rails 2.3.8 version. Thanks in advance for your help and support

    praneeth at 09 Feb 12 at 10:22

Got something to say?