13 February 2008

Auto-Complete Text Fields in Rails 2

[Note: this entry updated to include required routing, as I failed to mention that the first time.]

In Rails 2, the PrototypeHelper and ScriptaculousHelper code was removed to plugins. This means that doing auto-complete for various fields was no longer part of the base Rails. I didn't find any great, straight-forward docs on how to do this for Rails 2.x, so am sharing what I've done to hopefully help others. Further, the README for this tells you how to do it for standard string fields on a given object, but I needed to do it for an associated object field, so I'll cover that here as well. As you can guess, this only pertains to Rails 2.x...

First you'll need to install the auto_complete plugin:

script/plugin install auto_complete

This plugin will give you the text_field_with_auto_complete view helper method amongst others. It also provides a controller hook ( auto_complete_for) to implement the auto-complete action/method in your controller for you, if that works for the field you need. As per the auto_complete README, it looks like:
class BlogController < ApplicationController
auto_complete_for :post, :title
end

auto_complete_for, as used above, would implement a auto_complete_for_post_title method in your controller class. The parameters are the object and field/method of that object. The method it implements will dig through all the Post records in your database and do a LIKE comparison on the title column, comparing the title to the contents of the post[title] form field. With the results, it will generate the HTML for an unordered list (ul), and return that to the view.

This is pretty slick: you can essentially get auto-complete with a view method name change in your HTML (from calling text_field, to text_filed_with_auto_complete), and a single line added to your controller. Now, what happens if the field you want to auto-complete on doesn't directly correlate to a field on your model object? For example, in my case, I wanted to auto-complete on a field from a belongs_to association, so I couldn't use the pre-built auto-completion method that does a direct SQL query on the field.

This is actually easy to solve. You can just implement the auto_complete_for_object_field method yourlself. And, while you're at it, you might as well leverage some of the other helpful methods in the auto_complete plugin. Or, if you need custom view/HTML output, you can just render as you need within that method. When you do this, do not call the controller hook, simply implement the method yourself. Here's mine for example:
def auto_complete_for_doctor_organization
re = Regexp.new("^#{params[:doctor][:organization]}", "i")
find_options = { :order => "name ASC" }
@organizations = Organization.find(:all, find_options).collect(&:name).select { |org| org.match re }

render :inline => "<%= content_tag(:ul, @organizations.map { |org| content_tag(:li, h(org)) }) %>"
end

Finally, for completeness, here's the field's definition in my view template (Erb):
<label for="organization">Group/Practice/Hospital Affiliation</label>
<%= text_field_with_auto_complete :doctor, :organization, :autocomplete => "off" %>


The one counter-intuitive looking thing there is the :autocomplete => "off" bit. Uh, aren't we doing autocomplete? Well, this particular attribute tells the browser to not do it's auto form completion stuff (so that our code can do it instead). Oh, and of course you need to ensure you are including Prototype and Scriptaculous JavaScript libraries in your views, which you can achieve, rather bluntly, with:
<%= javascript_include_tag :all, :cache => true %>


Finally, you will need to add route(s) to your routing file. I use a sort of wild-card route to cover all my auto-completes:

map.auto_complete ':controller/:action',
:requirements => { :action => /auto_complete_for_\S+/ },
:conditions => { :method => :get }

This will resolve any routes that match an action starting with "auto_complete_for_".

With that, we have auto-completing text fields in Rails 2.x! Cool stuff.

44 comments:

vhtellez said...

thanks friend.
I appreciate that you did this


Lletrez

Anonymous said...

This is really useful stuff. Thanks!

Anonymous said...

Great help here. Thanks.

Anonymous said...

From the time I found your site via Google to the time I had auto-complete working in my site: eight minutes. That's awesome! Thanks for the well-described tutorial.

Chris said...

Glad this has been a help. If you'd like a twist on it, I'm now actually doing all my auto-complete stuff using jQuery and my modification of the Rails plugin for it. Why? a) I just like jQuery, but b) the autocomplete plugin for jQuery that I'm now using is awesome! It lets you do sub-text for your autocompletes (e.g. you could list the country underneath a city that matches or something - without affecting the values of the field), it caches, it's super fast, and it's unobtrusive JavaScript. I've written up how to use this in this entry.

Anonymous said...

is anyone else getting this:

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken)

Chris said...

I'd do some web searches on that error. The solution I used was simply to disable the filter for the auto complete action in the appropriate controller. You can do that in the controller that contains your auto complete action by doing:

skip_before_filter :verify_authenticity_token, :only => [:auto_complete_for_whatever]

Rashmi Pandit said...

Thanks a lot! I was up and running with this very quickly.

Anonymous said...

Thanks for this quick guide.

However, instead of fetching all items then selecting the appropiate ones from Rails, should find them by sql, else you will get a huge performance penalty. So instead of Organization.find(:all, find_options).collect... you should write Organization.find(:all, :conditions => ['name like ?', "%#{params[:organization][:name]}%")...

Chris said...

Zoli, very true. I don't actually use this anymore, as I now solely use jQuery, and have done a jQuery, as I mentioned in a previous comment. My jQuery auto-complete plugin specifically only fetches the column you're completing on, etc.

That said, I still often write my own auto-complete functions, because for example, with my current project, I show not just what the auto-complete matches, but below each one in italics I show further context (so, say you're matching cities, I show what state that city is in).

Anonymous said...

good tutorial but still was not working for me...

I came upon this page which does it a bit differently, it adds a route for the method and uses a get in the view, now it works for me too:

http://trix.pl/blog/auto-complete-for-rails-2-0-tutorial.html

Chris said...

Thanks anon, as someone else also ran into this, I've now updated this blog entry to include the route statement. Note that the above route definition is a single route that will cover all auto-complete actions.

Qaexl said...

Shouldn't that be:

find_options = { :conditions => [ "name LIKE ?", params[:doctor][:organization], :order => "name ASC" }
@organizations = Organization.find(:all, find_options)


Otherwise, doing it in-ruby-memory is going to bloat up your production server. Unless you're using some sort of magic Ruby-to-SQL thing, of course ...

Chris said...

Yes it should. In my newer stuff, which uses jQuery instead, etc. I do it in database. The reality for me at least is that in reality I rarely get to use a plugin for auto-completes, because I need custom functionality that can't be done easily in a stock auto-complete plugin (at least without pretty much overriding the entire point of the plugin :)

Tarun said...

Thanks a lot :)
I have quite a lot of belongs_to and has_many associations, and was wondering how to get autocomplete on them...

your tutorial helped a lot, especially the map to be added to the routes file.. because none of custom methods for reachable without it...

Robert Dempsey said...

Quick question - when I select one tag from the drop down all is well, however, when I select a second tag it removes what I have in the autocomplete field. How can I simply add to the list of tags in the field? Thanks.

Unknown said...

This isn't set up to do multiple tags. You'd need to adjust the JavaScript to amend the tag to the existing value, instead of replacing it.

Anonymous said...

@robert dempsey:

actually if you add the option :tokens => "," to the text_field_with_autocomplete method, it will append the selected li after a tag comma, instead of replacing the entire field value.

Anonymous said...

but auto-complete features need to get updated due to many new websites that provide blank fields to ber filled out manually which a little annoying on that part, but other than that, thanbks for all the great help and keep up the good work!

Thanks.

Anonymous said...

FYI I did some work to enable auto_complete to work with repeated text fields, such as child records in a complex form - some of you might find this useful. See: http://patshaughnessy.net/repeated_auto_complete

e12win said...

great article. thanks a bunch

hiroshi said...

For now, you should better to install the plugin:

script/plugin install git://github.com/rails/auto_complete.git

Anonymous said...

any way to add some extra ajax options? like I'd like to do:

text_field_with_auto_complete :sku, :name, :autocomplete => "off", :before => "Element.show('\
spinner')", :complete => "Element.hide('spinner')"

but the doesn't seem to work on my page

Anonymous said...

anyway to make it where when the user clicks from the typeahead selection list, that it auto fires a submit action to the ecapsulating form - instead of selecting the text and then having to hit a submit button?

thanks

Chris said...

Sergueï, yes, you want to use the jQuery version :) Or, with jQuery, I tend to just roll my own, as it's easy enough. Regardless, here are a few resources:

my jquery autocomplete plugin on GitHub

my most up to date blog entry on it and the original blog entry on the jQuery version

Basically, though, I just write the methods myself in my current work, as I typically need something more custom, but depends on your needs. The jQuery plugin I refer to in my plugin is the one I'm using though.

Chris said...

Sergueï, I would actually suggest you ditch this one and use the jQuery autocomplete instead, given that you have jQuery in your app. I think the jQuery approach is better.

For the hidden field, at least in the jQuery version, what I do is return the ID of the item as the "data" portion of each element that I send back from my autocomplete method. Then, my JavaScript results handler for that takes that data element and populates the hidden field with it. I do this a fair bit in my current app. I also have ones where the "data" element actually has two or three pieces of info in it, and I update two or three form fields (some hidden, some not) with that info.

I don't use the non-jQuery version at all anymore, and haven't for probably almost a year, so I'm just not up to speed with it, or with Prototype & Scriptaculous, etc. I only use jQuery now and have removed Prototype and Scriptaculous from my apps. Just works better for me.

Anonymous said...

Chris: anyway to make it where when the user clicks from the typeahead selection list, that it auto fires a submit action to the encapsulating form - instead of selecting the text and then having to hit a submit button?

Chris said...

txbarbarossa, I don't use the Prototype/Scriptaculous stuff or this plugin anymore, I just use jQuery (and I have a jQuery version of the plugin). So, it won't be a mod I do. That said, what I'd suggest is just adding a JavaScript event listener for the "change" event on the field that you're populating, and then have that handler do a submit or whatever needs to happen.

Anonymous said...

chris,

yeah it's weird i've tried that, but instead of the autocompleted text, it submits to the form the text the user typed that generated the autocomplete text instead of the full autocomplete result.

e.g.:
text_field_with_auto_complete :sku, :name, :autocomplete => "off", :onblur => "this.form.submit();"

Chris said...

txbarbarossa ya, I realize i'm wrong on that, because blur probably gets activated as soon as the autocomplete kicks in, and it'll only have the user's typed value at that point. I'm not sure in Prototype the exact thing to use, but if the autocomplete JavaScript code has a callback for when it gets results, then you could just tell the form to submit at the end of that handler (possibly, may or may not be quite the right time, but hopefully it's populated the results into the form by then).

Probably worth googling for a bit, as I'm sure others have done this kind of thing. Let me know if you find a good solution.

Andy Milk said...

Is it possible to use 2 of these on the same page for the same field? I've tried but only one functions

Tarun said...

@andy, could you please clarify on two of these for the same field ?

you can have any combination of auto-complete functions and form fields.

But you can at once map only one auto-complete function to one field.

If you tried two different fields, check if any of the form field names, etc. are getting duplicated. There could be a conflict, rename one of them.

Arvind said...

Hi there!

I am using auto-complete to provide possible search terms in a text-box (text search). The problem I'm facing is that whenever auto-complete opens up the suggestion list, it has already selected the topmost entry.
Now when I hit "Enter" on the keyboard - it will substitute my string with the first entry of the auto-completing list. This way I'm virtually unable to type in a search term that is a substring of one or more auto-suggested entries.

How can I make sure that the auto-suggested items are NOT selected unless i manually select them with either the mouse pointer or the arrow-keys?

Also - how can I make this faster?
I gave a pretty low number for the ":frequency", but that only results in it re-opening the auto-complete suggestion list immediately after I hit "Enter"... thus making me hit "Enter" multiple times before it finally stops that and lets the form be submitted.

How do I solve THAT too?

Thanks!

Chris said...

Arvind, honestly, I'm not sure on that. I don't use Prototype anymore, purely jQuery, so I do all my autocomplete stuff with jQuery instead. But, I know at least the jQuery plugin I use has that option as to whether the first item is selected by default or not, etc. Thus, I'd suggest poking around in the JavaScript code and seeing if there's an option for that, or an easy way to modify it, etc.

Gudata said...

Just to notice that

auto_complete_for_doctor_organization

method shown here is extermly slow code unusable for big tables.

Chris said...

Guduta, that's a valid observation for sure. As mentioned above, I don't actually use this anymore, and use jQuery, but even then, I don't even use a plugin, there's really no need, as these things are very simple to write yourself, and as you alude to, writing your autocomplete method specific to the implementation you need will yield the best results. At DealBase we have a couple fairly complex ones that do searches across joined tables and other such things. Performance can get tricky for sure.

Unknown said...

I'm using autoconmpleter plugin to help customer find book titles quickly, so they can add them to their favorites.

Is there a way that this plugin can Return the ID of the selected product ? So it can be passed to the form (and not having to re-search for the id based of the selected string) ?

Chris said...

Benji, just take the code that it uses by default, and implement you own autocomplete_... method that adds id's in - you'll need to separate them so that you still get the text back to display in the pulldown list. I don't use this anymore, and don't have the JS handy to look at at the moment, but you may be able to add the ID's with something like "list text|id" as the text you return and have the JavaScript that creates the pulldown menu rip off the pipe and ID at the end for display, etc. This is how the jQuery version I use works.

Unknown said...

Thanks for the tip Chris!

Anyway, I found this plugin (http://model-ac.rubyforge.org/) that does exactly what I need in the fly.

Pierrot said...

Thanks. This is really useful stuff. I was looking for a way to implement Ajax without having to meddle with javascript. I found it!

shikhap said...

hello, I want to add a default text to auto completer
Just like on google we get google writtin in background by default

Chris said...

shikhap - I would suggest checking out the jQuery form example plugin:

GitHub project/code page
home page

I use this to put in sample text in various form fields, but most often in search fields. It's super easy to use, allows styling, etc.

shikhap said...

See the implementation is already been done So I cant change the code from begining

my code is like
in html page I have following =
model_auto_completer("clip_geog_search", "","clip_geog_id", "", {:action => :classification_geogs_search}, {:style=>"width: 500px;"},{:min_chars=>3})

and I want "Click Here" to be wriiten in background once the textbox get's the focused clickhere will get removed and the , just like we have in google tool bar

Chris said...

shikhap - the jQuery plugin I mentioned has nothing to do with the Rails part of the code, it does it completely via JavaScript and CSS, so the Ruby/Rails implementation has no bearing on it. You would do this by having JavaScript similar to:

$('#clip_geog_id').example('Click Here');

That's it, then you will get the exact behavior you want: it will say Click Here in the field, and when someone clicks in there, that text will go away, and you will get the regular autocomplete behavior.

This of course only works if you're using jQuery. If you're using Prototype or some other JavaScript library, look for a similar default text type of plugin.