One of the problems mentioned by a couple people when I asked for suggestions on improving djangosnippets.org was the proliferation of tags. This is a well-known problem on sites that allow users to enter their own tags, where misspellings are frequent and its sometimes unclear whether a tag should be plural or singular.
To try and reduce the amount of different tags on djangosnippets I ended up using the jQuery UI autocomplete tools to provide users with hints when they enter tags for their snippets.
How it works
There are two components, a view that returns JSON based on the partial tag that the user has typed in, and some JavaScript that fetches the results and updates the input element. The jQuery autocomplete stuff is a little opaque and seems well-designed for the simple case but when it comes to extending it the docs weren't very helpful. I looked at Nathan Borror's autocomplete helper for some of the ideas on extending the default functionality.
The view is pretty straightforward. It takes the incoming request and peels off the query, then filters the database of Tags for those matching the query adding a "count" of how many snippets are using that tag.
def tag_hint(request):
q = request.GET.get('q', '')
results = []
if len(q) > 2:
tag_qs = Tag.objects.filter(slug__startswith=q)
annotated_qs = tag_qs.annotate(count=Count('taggit_taggeditem_items__id'))
for obj in annotated_qs.order_by('-count', 'slug')[:10]:
results.append({
'tag': obj.slug,
'count': obj.count,
})
return HttpResponse(json.dumps(results), mimetype='application/json')
The client-side code is a little bit more complex since it is not really following the default behavior of the jQuery autocomplete library. I'll go through the code bit by bit starting with the class definition:
Snippets = window.Snippets || {};
(function(S, $) {
var TagCompletion = function(options) {
this.options = options || {};
this.default_url = '/snippets/tag-hint/';
};
// other code here...
S.TagCompletion = TagCompletion;
})(Snippets, jQuery);
This simply allows us to initialize our class, which is exported into the global namespace:
<script type="text/javascript">
$(function() {
var tag_completion = new Snippets.TagCompletion();
// additional initialization happens here
});
</script>
We need to add some code to actually do the autocompletion when a user starts typing into the tags input, so we'll add a function to bind a listener to the tag input:
TagCompletion.prototype.bind_listener = function(input_sel) {
var self = this;
this.input_element = $(input_sel);
this.input_element.autocomplete({
minLength: self.options.min_length || 3,
source: function(request, response) {self.fetch_results(request, response);},
});
};
The source attribute is part of the autocomplete API that allows you to specify a custom source for the data displayed to the end user. We're using the tag hint view, so we need to set that up. The catch is that we don't want to autocomplete on the entirety of the text in the tags input, but just on the bit after the last comma. So we'll need to split the incoming value of the input on commas, then find the last word and send it off to our view:
TagCompletion.prototype.fetch_results = function(request, response) {
var url = this.options.url || this.default_url,
term = request.term,
last_piece = get_last_piece(term);
if (!last_piece)
response([]);
var pieces = clean_and_split(term),
all_but_last = '';
pieces.pop();
// if there are already some tags present, then grab everything up to the
// current phrase and add a comma
if (pieces.length > 0)
all_but_last = pieces.join(', ') + ', ';
$.getJSON(url, {'q': last_piece}, function(data) {
var results = [];
$.each(data, function(k, v) {
v.label = v.tag + ' (' + v.count + ')';
v.value = all_but_last + v.tag + ', ';
results.push(v);
});
response(results);
});
};
/* helper functions for splitting the string */
function clean_and_split(text, delimiter) {
var cleaned = [],
pieces = text.split(delimiter || ',');
for (var i = 0; i < pieces.length; i++) {
if (pieces[i].match(/\w+/)) {
cleaned.push(pieces[i].replace(/^\s+|\s+$/, ''));
}
}
return cleaned;
}
function get_last_piece(text) {
var cleaned = clean_and_split(text, ',');
if (cleaned.length > 0) {
var last = cleaned[cleaned.length - 1];
if (last.match(/\w+/))
return last;
}
};
Now, just bind the TagCompletion class to the tags input and we're good to go:
<script type="text/javascript">
$(function() {
if ($('#id_tags').length > 0) {
var tag_completion = new Snippets.TagCompletion()
tag_completion.bind_listener('#id_tags');
}
});
</script>
Hopefully this will lead to more useful tags on the site!

Comments (2)
This is great Charles, thanks for laying out how you did it. I'm looking forward to this and the other changes that make it into Djangosnippets.
Did you know there is an app that does autocomplete for django-taggit?
https://github.com/Jaza/django-taggit-autocomplete
You're doing more work with Javascript I guess, I wonder if there are advantages one way or the other?
Thanks for your comment and the link, Andrew! I guess I should have googled before I wrote this stuff. Here's a link to the jQuery UI example of how they did the multiple values:
http://jqueryui.com/demos/autocomplete/multiple.html
Commenting has been disabled for this entry
If you'd like to discuss an aspect of this post, feel free to contact me via email.