Modifying auto_complete for realistic use

2008-06-26 20:00:00 -0400


We use the Rails auto_complete plugin in both PingMe and Tempo, and while it’s incredibly handy, it takes a little bit of hacking to use it on a page where it will be used more than once. In the main time screen on Tempo any of the entries can be opened for editing, meaning more than one can be open at once, and each has its own auto complete field for tags:

Similarly in PingMe, we’ve got a list of pings on the screen, each a potential edit form (more than one can be edited at once), with auto complete on the tags field:

In this situation, when each form has the text_field_with_auto_complete in use, each has the same DOM ID: object_method, so ping_tags in PingMe and entry_tags in Tempo. Screwy things happen when you try to remotely update a DOM ID used more than once on a page, and those screwy things tend to change based on your browser, but basically the auto_complete plugin out-of-the-box will not work in this scenario.

We got around this by doing a bit of monkey patching to the auto complete plugin. This is what the initial text_field_with_auto_complete method looks like inside the plugin’s auto_complete_macros_helper.rb:

def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {})
(completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
text_field(object, method, tag_options) +
content_tag("div", "", :id => "#{object}_#{method}_auto_complete", :class => "auto_complete") +
auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options))
end

What we do is add an extra, optional parameter to this method that accepts an id, that we then patch in as the dom id we’ll use:

module AutoCompleteMacrosHelper
def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {}, object_id = nil)
field_id = (object_id.nil?) ? "#{object}_#{method}" : "#{object}_#{object_id}_#{method}"
(completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
text_field(object, method, tag_options.merge({:id => field_id})) +
content_tag("div", "", :id => "#{field_id}_auto_complete", :class => "auto_complete") +
auto_complete_field(field_id, { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options))
end
end

Now, on the form/template itself, we use this to generate the text field and type ahead div (we’re using HAML, not RHTML):

= text_field_with_auto_complete :entry, :tag_s, {:size => 50}, { :indicator => "entry_#{entry.id}_tag_s_form_loader", :frequency => 0.4, :tokens => ' ' }, entry.id
= image_tag 'loading.gif', {:id => "entry_#{entry.id}_tag_s_form_loader", :style => 'display: none;'}
.auto_complete{:id => "entry_#{entry.id}_tag_s_auto_complete", :style => "display: none;"}

You’ll notice that each dom id is of the format entry_#id_tag_s_*.

In our controller, we have a few things that we need, nothing that complex:

auto_complete_for :entry, :tag_s

def auto_complete_for_entry_tag_s
unless params[:entry].nil? || params[:entry][:tag_s].blank?
tags = @current_user.tags.collect{|tag| tag.name.downcase.index(params[:ping][:tag_s].downcase) == 0 ? tag.name : nil}.compact
render :partial => 'autocomplete', :locals => { :items => tags }
end
end

Finally, we create a partial called ‘_autocomplete.haml’ that cranks through the items when the suggestions are displayed:

%ul.autocomplete_list
- items.each do |item|
%li.autocomplete_item= h item

Happily, this isn’t very different from the way the plugin recommends you do things, but allows you to support more than one field on the page at once when you have multiple instances of an object. I’m actually a bit surprised that there isn’t an inherent facility in the plugin for this, but it’s good learning for anyone looking to see how it’s done.

I should note that the above doesn’t denote how we really select out the set of tags for a user in Tempo, but it’s a simple example of how one might do it ;-)


Deep Thought

2008-06-25 20:00:00 -0400


With the new smart timers, I no longer need PingMe to remind me to sum up my time for the day. Thanks, old buddy!

(Although the street cleaning reminders are still pretty clutch…)


Deep Thought

2008-06-24 20:00:00 -0400


Git log commits remind me of summer afternoons. Like last week.

(Guess who hasn’t updated his time in a bit?)

((The new timer helps keep me straight nowadays!))


Deep Thought

2008-06-12 20:00:00 -0400


A very simple open-source support ticket system, transparently integrated with e-mail, that isn’t written in Python would be a fantastic thing to have and use.


Introducing acts_as_union

2008-06-11 20:00:00 -0400


While doing a bit of work on Tempo recently, we found we had the need to provide a union’ed set of Active Record associations on a model. We already had the code in place in acts_as_network to do something very similar via the UnionCollection class, so it was a simple step to create ActsAsUnion within the same plugin.

Here’s an example of what we mean and how it works:

class Person < ActiveRecord::Base
acts_as_network :friends
acts_as_network :colleagues, :through => :invites, :foreign_key => 'person_id',
:conditions => ["is_accepted = 't'"]
acts_as_union :aquantainces, [:friends, :colleagues]
end

In this case a call to the aquantainces method will return a UnionCollection on both a person’s friends and their colleagues. Likewise, finder operations will work accross the two distinct sets as if they were one. Thus, for the following code:

stephen = Person.find_by_name('Stephen')
# search for user by login
billy = stephen.aquantainces.find_by_name('Billy')

both Stephen’s friends and colleagues collections would be searched for someone named Billy.

To obtain acts_as_union, simple install the newest version of the acts_as_network plugin and use as shown above. The acts_as_union method does not accept any options.