Auto-completion for tag lists
January 31st, 2007 by michael
See the end of this post for an important update.
Standard Rails auto-completion is geared toward completing a single value entered in a text field. For most cases this is exactly what’s needed, but there is one prominent case that is different: auto-completion of tags. If something can have a tag at all, it’s part and parcel of the deal that it can have multiple tags.
As far as I can tell, some configuration tweak is not enough to make standard auto-completion do our bidding. But not all is lost. It’s just a small matter of programming to get what we want.
I’m assuming that you’re using Acts As Taggable or Acts As Taggable On Steroids for the model-level tagging, or otherwise that you know what you’re doing.
First, we need client-side support for picking out only the tag we’re currently editing.
Ajax.TagAutocompleter = Class.create();
Object.extend(Object.extend(Ajax.TagAutocompleter.prototype, Ajax.Autocompleter.prototype), {
getToken: function() {
var range = this.rangeForCaret();
var value = this.element.value.substring(range.start, range.end);
return value;
},
updateElement: function(selectedElement) {
var oldValue = this.element.value;
var range = this.rangeForCaret();
var prefix = oldValue.substring(0, range.start);
var suffix = oldValue.substring(range.end);
var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
this.element.value = prefix + value + suffix;
this.element.focus();
},
rangeForCaret: function() {
var value = this.element.value;
var start = 0, end = value.length;
var pos;
if (typeof this.element.selectionStart != 'undefined') {
pos = this.element.selectionStart;
} else if (document.selection) {
var sel = document.selection.createRange();
var selLength = sel.text.length;
sel.moveStart('character', -(value.length));
pos = sel.text.length - selLength;
}
if (pos != undefined) {
end = pos;
while (value.charAt(end) != ',' && end < value.length) {
end++;
}
start = end - 1;
while (value.charAt(start) != ',' && start > 0) {
start--;
}
while (start < end && (value.charAt(start) == ' ' || value.charAt(start) == ',')) {
start++;
}
while (start < end && value.charAt(end) == ' ') {
end--;
}
}
return {start: start, end: end};
}
});
Yes, the code of rangeForCaret is a bit convoluted, let’s say traditional JavaScript-style. The general approach for getting the position of the text cursor (“caret”) is explained here. The purpose of all the stuff is to support auto-completion in the middle of the text field, not just at its end.
On the server-side, in the relevant controller, there’s still some work to do as the standard auto_complete_for can’t handle tags. Put the following method in your controller base class, app/controllers/application.rb.
def self.tag_auto_complete_for(object)
define_method("auto_complete_for_#{object}_tag_list") do
@items = Tag.find(:all,
:conditions => [ 'LOWER(name) LIKE ?', '%' + params[object][:tag_list].downcase + '%' ],
:\order => 'name ASC',
:limit => 10)
render :inline => "<%= auto_complete_result @items, :name %>"
end
end
Then, in an individual controller, create the tag auto-completion method like this
class PeopleController < ApplicationController
tag_auto_complete_for :person
end
Almost done!
What’s still missing is a way to activate auto-completion for the tag input field. It would surely be possible to copy & change text_field_with_auto_complete or to monkey patch it appropriately. I prefer another way, in fact, I’m not using text_field_with_auto_complete at all, not even where it would work. Instead, I prefer more unobtrusive way.
The way starts with the class autocomplete to text fields that are amenable to auto-completion — including, of course, the tag list field.
<%= f.text_field :tag_list, :class => 'autocomplete' %>
As this in itself is completely inert, we need to nudge it a bit in application.js. Here’s a function that looks for all text fields with class autocomplete and actually makes them do what they say.
function installAutocompletion() {
$$('input.autocomplete[type=text]').each(function(element) {
var fieldId = element.id;
var completions = document.createElement('div');
completions.id = fieldId + '_auto_complete';
completions.className = 'auto_complete';
completions.style.display = 'none';
element.parentNode.insertBefore(completions, element.nextSibling);
var url = 'auto_complete_for_' + fieldId;
if (/_tag_list$/.test(fieldId)) {
new Ajax.TagAutocompleter(element, completions, url);
} else {
new Ajax.Autocompleter(element, completions, url);
}
});
}
Be sure to call this method some time after the page is loaded. If you don’t have a function from where to call it, add this to application.js
Event.observe(window, 'load', installAutocompletion);
Update, 2007-02-05
I may not have seen the wood for the trees when looking at the original code of the Rails/Script.aculo.us autocompleter. Of course it is fully capable of auto-completing more than one token in the same text field — with a very little configuration tweak, no less.
What it can’t do is auto-completion in the middle of the line; it only works at the end. That’s an acceptable restriction, to my mind. So, forget about Ajax.TagAutocompleter from above and instead install your autocompleters like this
function installAutocompletion() {
$$('input.autocomplete[type=text]').each(function(element) {
var fieldId = element.id;
var completions = document.createElement('div');
completions.id = fieldId + '_auto_complete';
completions.className = 'auto_complete';
completions.style.display = 'none';
element.parentNode.insertBefore(completions, element.nextSibling);
var url = 'auto_complete_for_' + fieldId;
var options = {};
if (/_tag_list$/.test(fieldId)) {
options.tokens = ',';
}
new Ajax.Autocompleter(element, completions, url, options);
});
}

Michael,
Thanks for sharing this script. I ran into a problem with your installAutocompletion js (2007-02-05 version). It appears that Rails tries to execute the current url and pass auto_complete_for_xxx as the id. How did you get around this problem?
Tron
You probably don’t have a route for the
auto_complete_for_xyzaction. Try adding this line toconfig/routes.rbmap.connect ':controller/:action', :requirements => { :action => /auto_complete_for_.+/ }Thanks for the quick response. I added the route you suggested. It is still not to going to the auto_complete_for_xxx action. It just goes to /admin/books/edit and passing auto_complete_for_xxx as the id.
Thanks,
Tron
Well, it boils down to this: check which URL is requested by the auto-complete request and make sure there is a route that recognizes it.
Thaqnks michael, here are the two changes I made to make it work:
1) In the installAutocompletion function change the line from
var url = ‘auto_complete_for_’ + fieldId;
to
var url = ‘/auto_complete_for_’ + fieldId;
2) In routes.rb add the line
map.connect ‘/:action’, :controller => ‘admin/books’, :requirements => { :action => /auto_complete_for_book_.+/ }
Now, everything works as advertised.
I’m puzzled, because I began using del.icio.us in December 2005, and it already had auto-completion, in both Windows and Mac browsers. It would even auto-complete tags typed somewhere in the middle of the line of text tags.
Could it be that I was using a browser-based feature without realizing it? I don’t think so. I learned about auto-completion in del.icio.us from their Help pages, when I started using it.
The only reason I found this blog was because auto-completion is broken, today, in del.icio.us, with the advent of their new design. I hoped to find a solution.
Mooncaine, most modern browsers have an auto-completion feature, therefore, if you didn’t handle auto-completion explicitly that’s what you may have seen. The Scriptaculous Ajax.Autocompleter takes care to disable the automatic feature, however.
I don’t know of any recent breakage, but that may well be because I’m not tracking the bleeding edge.
Hi,
I have done according to the description( updated part but not include the large javascript code in view) but when when I add
Event.observe(window, ‘load’, installAutocompletion());
in application.js , it shows javascript error ” handler is null or it is not an object”
when I delete that line from the application error is vanished and auto complete is not working.
Moreover browser’s auto complete is working that is not acceptable. Please guide me. where is my mistake.
“but not include the large javascript code in view”
Please consider again what you are doing. Your code is not going to work if you don’t include all the code it relies on.