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
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?