Let me apologize for the title. I tried like heck but I couldn't find a better title for the issue I'm talking about. Personally I blame the apple pie. Let me describe the problem and how I used ColdFusion 8 Ajax features to solve it. If folks have a better title for the blog entry, I'll be sure to use it in the Spry/jQuery and maybe Ext versions I have planned.
Ok, so what in the heck am I talking about? Imagine a simple form that represents an entity in your database. For example, one project at RIAForge (Yes, it's still up). Projects have many different fields, most of which are simple text based fields like name and description. Some fields are pointers to other database tables. Projects can exist in categories so my form has an HTML select field for choosing related categories. Projects also have owners and this is where my main problem comes in. RIAForge currently has over 4,500 users. If I want to build a 'typical' way to edit the owner for a project I'd have a select drop down with 4,500 options. That probably would work since most of us have cable modems, but it's going to be a bit insane to actually use. That's why I actually don't change owners at RIAForge. I don't think anyone has ever asked and I realized early on that this would be a problem. So what could I do if I actually did want to let people change owners for a project?
I decided my first solution to this problem would use ColdFusion 8's built-in Ajax features only. I decided one simple way to handle it would be to display the current owner as plain text, but to then let you click a link to launch a browser of users. This would make use of cfwindow and have built-in pagination. Here is a screen shot of the final product. This is the form as it initially loads:

When you click Select User, the window control is activated:

And because I love me some gratuitous Flash, here is a screencast of the solution in action:
Note - the embed below is working oddly. I haven't used Jing in a while. Click in the triangle in the upper left hand corner to start the movie.
Ok, so whats going on here? The main page, test_largeds.cfm, is a faked out form. I wanted something folks could download and play with (download link at the bottom) so I didn't use a real database. Most of the code is vanilla form, so I'll dump it here and explain the interesting parts.
<cfparam name="form.name" default="Some Value">
<cfparam name="form.foo" default="Some Other Value">
<cfparam name="form.useridfk" default="1">
<cfdump var="#form#" label="Form" />
<cfoutput>
<form action="#cgi.script_name#" method="post">
<table>
<tr>
<td>Name:</td>
<td><input type="text" name="name" value="#form.name#" /></td>
</tr>
<tr valign="top">
<td>Owner:</td>
<td>
<input type="hidden" id="useridfk" name="useridfk" value="#form.useridfk#" />
<span id="ownerspan">User 1</span><br/>
<a href="" onClick="ColdFusion.Window.show('userlist');return false">Select Owner</a>
</td>
</tr>
<tr>
<td>Foo:</td>
<td><input type="text" name="foo" value="#form.foo#" /></td>
</tr>
<tr>
<td> </td>
<td><input type="submit" name="save" value="Save" /></td>
</tr>
</table>
</form>
</cfoutput>
<cfwindow center="true" draggable="true" modal="true" name="userlist" source="test_userlist.cfm" title="Select Owner" width="250" height="280" />
The name, foo, and useridfk values are all hard coded. Name and foo are simple text fields just for comparison. The owner field is a bit different. I used a hidden form field for the owner's ID value. Below it I output his name (again, it's only hard coded here because this is a demo). Next is a link that is used to activate the cfwindow defined at the very bottom of the page. The window points to test_userlist.cfm. Let's take a look at that now:
<cfparam name="url.start" default="1">
<cfset total = 92>
<cfset perpage = 10>
<script>
selectUser = function(id,label) {
document.getElementById('ownerspan').innerHTML = label;
document.getElementById('useridfk').value = id;
ColdFusion.Window.hide('userlist');
}
</script>
<cfloop index="x" from="#url.start#" to="#min(url.start+perpage-1, total)#">
<cfoutput>
<a href="" onClick="selectUser(#x#,'Record #x#');return false;">Record #x#</a><br />
</cfoutput>
</cfloop>
<cfoutput>
<cfif url.start gt 1>
<a href="#ajaxLink('#cgi.script_name#?start=#url.start-perpage#')#">Previous</a>
<cfelse>
Previous
</cfif>
/
<cfif (url.start+perpage-1) lt total>
<a href="#ajaxLink('#cgi.script_name#?start=#url.start+perpage#')#">Next</a>
<cfelse>
Next
</cfif>
</cfoutput>
Most of the code was ripped from an earlier blog entry and takes care of outputting a page of hard coded data. There are a few critical things to note here. First, the pagination makes use of ajaxLink. This keeps the linked content within the cfwindow container. Next, notice how each person's record is a link to a function named selectUser.
The selectUser function takes an ID and a label. I then use getElementById to grab the span defined in the previous template as well as the hidden form field. Once the values are updated I simply hide the window.
This is a simple demo and could be improved with a bit more functionality. Using RIAForge as the example, I probably would not want to "Next" through more than 4500 records. Adding a simple name filter on top would help reduce the amount of pages I'd have to flip through.
Thoughts? As I mentioned at the beginning of this post I plan on discussing similar solutions in jQuery and Spry as well.
Archived Comments
What about using auto-suggest, like on your project search page?
Ray, nice solution. (I think the previous comment is spam - I got the same exact one on my site).
Nice example. I recently had to do something like this.. was trying CF8's autosuggest but couldn't figure how to pass an ID value. Ended up using Scriptaculous.. the afterUpdate function makes it real easy to pass in an ID
http://github.com/madrobby/...
Spam comment nuked. Thanks.
@JM: Issue with autosuggest is that you get the string back. So if I typed in "Ray" and it matched Raymond Camden, I would have no idea what ID Raymond Camden is. I need to display a name, but store a PK value.
(Reading Jeff's comment) - So yeah, what Jeff encountered is the same. No way to do both a complex result.
I use the html-based cfform cfinput autosuggest feature when dealing with fields that become unwieldy in selects. The key is to identify who the user will be and how they best operate. For example I started by setting the autosuggestBindDelay = 1 [sec] and the autosuggestMinLength = 1, but users that type fast became frustrated till I increased the values to 5sec and 3 respectively. Now a user who types fast and knows what they are typing never sees the autosuggest, but those that are unsure will get help as well as postive feedback that a value exists. I use a select in combination with an autosuggest for fields that are small enough (<1 or 2k records) - since I don't have dialup modem users. I also use this field to allow the user to add a new value, which is not applicable in your case. Also with autosuggest you have to watch out if your values have a leading zero and append a space on the return or it will get removed.
I meant prepend a space.
Brian, how do you deal with ID versus label issues? Given that "Raymond Camden" is PK 3, and your autosuggest only shows Raymond Camden, how do you handle that when you save the data?
When I chose to use the autosuggest in which I bring back a string, I made the decision to not pass keys only the string values. Then when I use the chosen values I just pass them back to my CFC and do my lookup there if further processing is needed. The lookup is very fast. I have not use Jquery, Spry or Scriptaculous. My toolkit is growing, but still limited.
I guess I'm confused. If I were to do this, I'd return a query object (JSON) from the Ajax call containing all the data that I need, and then I'd fill out the HTML table using DOM add/remove methods (using JS). So you'd have both the string and the ID in the results, and you'd put them both into the href table row values. But I might be missing something important here...
@Jake: So you would return 4000 rows of data in JSON? Even with 'slim' JSON, that's a bit much. :)
No, I'd return the subset of records that match the partial string. In fact, to reduce the result set further, you could only return records after there are at least 3 characters, something like that.
True, that could work too. It would be cool to mix both in as I mentioned in the blog entry. Have the pagination along with a simple filter on top.
That's funny ... when I first saw the form in Ray's example, before I saw the details, that's what I thought it was going to do, let you page through results that matched what you typed.
Either solution separately is good; combined, it would be better, for sure, and you don't need 3000+ records to make it worthwhile. Once you get more than one or two "pages" in a select box (as long as it's not the IE select box that wants as much of the screen as it can get), it's starts to be more "hunt" than "select", and anything we can provide to make it more of a "select" box is good.
@Ray
This is a smart use of the cfwindow. I'm building an application right now where the user has to select an item number (there could be quite a few inventory items in the db) and I'm going to use the cfwindow + function combination like you've done. I think my lookup page will probably sport a cfgrid with some filtering action but same concept with a different display. Thanks for the post.
This is exactly what I was needed to do. Thanks Ray!