Populating more than one text box with Prototype's Autocompleter

It took more than 30 seconds of digging to get this done, so I thought I'd share a quick-n-dirty way to use Prototype's Autocompleter to update more than one text box when making a selection from the suggestion list. As an added twist, I'll also show you how to do this for a list of text boxes that can grow dynamically, so the text box's id isn't known.

Here's the scenario. Imagine you are creating an admin UI for a CD store, and when editing a CD you want to allow the admin the edit the tracks that belong to the CD. Additionally, you want to be able to click a button to add a new row via a javascript, so the admin can add multiple tracks with just one submit. A snippet of your view on the CD edit page might look something like this:

<table> <thead> <tr> <th>Song Name</th> <th>Artist Name</th> </tr> </thead> <tbody id="songs"> <%= render :partial => "song", :collection => @cd.songs %> </tbody> </table> <p> <%= link_to_function("Add a song", "addRow()") %> </p> <p> <%= f.submit "Save Changes" %> </p>

This is fairly standard stuff. You have a list of songs in a table rendered by a partial. The "Add a song" button calls the addRow function to insert some HTML in the table:

function addRow() { new Insertion.Bottom('songs', '< %= escape_javascript(render(:partial => "shared/song", :object => Song.new)) %>'); ... }

The partial has your typical text inputs rendered by the text_field helper. To make the song title auto-complete-able, though, we add the class "text_complete" to it, and add a div with the class "auto_complete" (which will hold the suggestions) right after it. And now, for the magic that allows you to auto-complete more than one text box at a time, when the text boxes are dynamically generated. Let's revisit that javascript:

var autocomplete_names = < %= Song.all.collect {|s| %Q{#{s.name}#{s.artist_name}} }.to_json %>

function addRow() { new Insertion.Bottom('songs', '< %= escape_javascript(render(:partial => "shared/song", :object => Song.new)) %>'); new Autocompleter.Local($$('.text_complete').last(), $$('.auto_complete').last(), autocomplete_names, { frequency: 0.1, select: 'song_name', afterUpdateElement: function(target, selected) { target.up().next().firstChild.value = Element.collectTextNodes($(selected).select('.artist_name')[0]); } }); }

I'm using the Autocompleter.Local class here, as I like to pre-load the page with the suggestions to keep things snappy. The first line of that javascript block does this, wrapping the song and artist names in spans that I can style and select on later when populating the text boxes. After the HTML from the partial is inserted, a new Autocompleter.Local is create that populates the last text box with the "auto_complete" class -- the one that just got inserted. Likewise, the just-inserted div is used for displaying the suggestions. The third argument is the array of song and artist names to use for the suggestion search. The select option in the fourth argument tells the completer which class to use to populate the primary text box (the song name), and the afterUpdateElement option uses an anonymous function to populate the artist name text box, which lives in the table cell next to the song name text box, if there is an artist name.

There are two caveats to this approach, though:

  1. I had to change line 455 of controls.js to match on \W rather than \s to still match at the beginning of the song name (ignoring the HTML markup in the array of suggestions)
  2. Since both the artist name and song name are in the search string, the auto-suggest could find artists matching what you typed in the song name input box. If I cared, I could solve this problem, but it's not a big deal in my case.

There you have it -- easy updating of more than one text box with suggestions, even for text inputs created on the fly. Enjoy!

Comments