I've blogged recently (see related entries below) on jQuery UI's Autocomplete control. It's a cool control and has something inherit that I really wish ColdFusion's built in control had - the ability to return complex data back to the form. As an example, this lets us return a label and an ID value. You can store the ID in a hidden field and display a name to the user. Even better, you can also a label value. This means you can have both a unique display in the drop down of suggestions that is different from what actually gets shown in the text field. This label can include HTML, which means you can do all kinds of cool stuff. You could - for example, have an autocomplete that uses colors to convey information (perhaps highlighting results that other customers have selected). I whipped up a quick example that shows a cool example of this.
For my new demo I created a search form for my blog comments. The idea would be that you can begin typing a name and see suggestions from the folks who have commented on my blog. The front end code for this is a very slightly modified version of the final example in this blog post.
<script type="text/javascript">
$(function() {
$("#name").autocomplete({
source: "blogservice.cfc?method=searchpeople&returnformat=json",
select:function(event,ui) {
$("#email").val(ui.item.email)
}
});
});
</script> <form action="test4.cfm" method="post">
name: <input name="name" id="name" />
<input name="email" id="email" type="hidden">
<input type="submit" value="Submit">
</form> <cfif not structIsEmpty(form)>
<cfdump var="#form#" label="Form">
</cfif>
<script src="jqueryui/js/jquery-1.4.2.min.js"></script>
<script src="jqueryui/js/jquery-ui-1.8.custom.min.js"></script>
<link rel="stylesheet" href="jqueryui/css/vader/jquery-ui-1.8.custom.css" type="text/css" />
The only thing different here from the earlier blog entry is that I'm pointing to a new service now and I'm using email as the field I'll be storing behind the scenes. Again - if any of this is weird to you, please reread the earlier entry. Now let's look at the service.
remote function searchPeople(string term) {
var q = new com.adobe.coldfusion.query();
q.setDatasource("myblog");
q.setSQL("select distinct name, email from tblblogcomments where name like :search limit 0,15");
q.addParam(name="search",value="%#arguments.term#%",cfsqltype="cf_sql_varchar");
var query = q.execute().getResult();
var result = [];
for(var i=1; i<=query.recordCount; i++) {
result[arrayLen(result)+1] = {};
result[arrayLen(result)]["email"] = query.email[i];
var gimage = "http://www.gravatar.com/avatar/#lcase(hash(query.email[i]))#?s=20&r=pg";
result[arrayLen(result)]["label"] = "<img src='#gimage#' align='left'>" & query.name[i];
result[arrayLen(result)]["value"] = query.name[i];
}
return result;
} }
component {
Again - this is pretty similar to the previous entry, but notice that I've added a new label field to my result. Because the label allows for generic HTML, I've prefixed the name value with the Gravatar for their email. I wasn't sure how well this would work, but wow, it worked like a charm.
Edit: I modified the service code to always return my email address. I didn't mean to expose my commenter's email addresses like that. I apologize, and thank you to Todd Rafferty for pointing it out!
Archived Comments
Ray, I don't want to complain about the useful demo, but you're exposing people's email addresses in the dump. :)
Ugh, your right. Will fix it up.
Updated. Thanks again Todd!
No problem, you can delete my comments. *waves hand* this comment thread never happened.
Ray, very usefull post, of course.
i noted that the autocomplete works searchs all occurrence of the typed text: if you type 'aba' it will retorn all occurences of this string, everywhere in the name.
is it possible to change thes behaviour, using wild card:
1) 'aba' returns name aba
2)?aba (or ??aba or ?aba? ) with ? standing for 1 character?
3)*aba (names ending with aba)
4)aba* (names staring with aba)
4)*aba* (names containing aba)
regards
Yes - you can do any of those. Since you control the search, you control the results.
Very nice and handy for lazy me thanks !
Thanks for providing me with those valuable html codes.
Thanks for the helpful information. Your example worked perfectly, although I have one caveat: if you have debugging information turned on in CF admin, the autocomplete will not work since the data returned will no longer be JSON data (it will have the debugging information appended to it). You can get around this by using <cfsetting showdebugoutput="false" /> in your remote method or by writing a CFML template as a wrapper for that method, again using <cfsetting>. I do not think there is an equivalent for CFSETTING in CFSCRIPT however.
It took me a couple of days to figure out why my autocomplete was working for some people and not for others, so hopefully this comment is of help to someone else out there.
Thanks for that this is great
I have spend hours trying to understand why my html code in the label variable was showing as <img src......
and it looks like there is a bug with later version of the ui
mine was using
<script src="jqueryui/js/jquery-ui-1.8.4.custom.min.js"></script>
so i took yours from this demo and it worked
<script src="jqueryui/js/jquery-ui-1.8.custom.min.js"></script>
Not sure why this was happening but it is solved now.
thanks for putting this together.
I wanted to put a post out there for anyone else who might be new to JSON like myself...
I was pulling my hair out trying to figure out why the autocomplete data was not being displayed. Firebug showed that data was being returned and I couldn't figure out why.
Then I looked a little closer at firebug and saw this...
//[{"brandID":113,"value":"Arte Cubano"},{"brandID":11,"value":"Arturo Fuente"}...]
I then realized that it was not displaying because in the CF Administrator settings I had "Prefix serialized JSON with //".
As soon as I disabled that in the Administrator, it worked beautifully.
Nothing earth-shattering, but I hope it saves someone an hour and hair.
As just an FYI, when you use CF's built in Ajax controls, it knows this and strips it away. And if for some reason you could not turn it off (like on an ISP), you could tell jQuery to remove it too.
Just ran across your insideRIA article about ajaxSetup -- problem solved.
Thanks.
http://insideria.com/2009/0...
Ray,
I'm new to jQuery...
When I clear the text from autocomplete, I also want to clear the value of the hidden field.
How might I be able to get that done?
Thanks.
Probably by adding this:
$("#name").change(function(e) {
if($(this).val() == "") $("#email").val("");
});
I wrote this by hand so it may have a typo.
Just so folks know, outputting HTML in the autocomplete box seems to have changed/broken in a later release of jQuery UI (not sure which version). Ray's example here uses v1.8 and it works fine. I'm using 1.8.7 and it converts the opening and closing tags to their xml-formatted equivalent (less than and great than symbols get converted to <, etc). Not sure if this is a feature or a bug, but something to keep in mind.
If anyone knows of a good workaround, please post it here for others to enjoy :). I was thinking of writing some js to loop over the results and attempt to convert it back to HTML, but was too lazy :).
Thanks for letting us know, Jeff!
Posted a workaround for post-1.8 here:
http://wp.me/pBHhc-eY
Thanks. It's too bad this change was made. It seems a bit silly.
Hoping someone encountered a similar issue with jquery autocomplete. My code is very similar to the example however my cfc is returning an array (1dim). All is good with character data; however my results are mostly numeric. This seems to confuse the autocomplete... note my response from firebug..
[1000110.74,1000110.75,1000124.01,1000224.64,1000225.64,1000230.34,1000230.74,1000232.17,1000239.17,1000239.2,1000289.17,1000289.2,1000547.85,1000627.44,1000627.45,"1000627.6a9 ",1000627.85,1000627.88,1000717.16,1000745.88]
Notice the only item displayed in my autocomplete is the result found in double quotes (above).
My data column from the query is varchar defined datatype.
Anyone think of a work-a-round?
Of course the post cuts out the part needed... here is the response data again broken up.
[1000110.74,1000110.75,1000124.01,1000224.64,1000225.64,
1000230.34,1000230.74,1000232.17,1000239.17,1000239.2,
1000289.17,1000289.2,1000547.85,1000627.44,1000627.45,
"1000627.6a9 ",1000627.85,1000627.88,1000717.16,
1000745.88]
What I came up with: return an array of structs.
Works (with char & numerics):
<cfset var result = arrayNew(1) />
<cfloop query="qryName">
<cfset timStruct = StructNew() />
<cfset timStruct["label"] = qryName.column/>
<cfset arrayAppend(result, timStruct) />
</cfloop>
Does not work (with numerics):
<cfset var result = arrayNew(1) />
<cfloop query="qryName">
<cfset arrayAppend(result, qryName.column) />
</cfloop>
Just a little warning.
I volontary stayed at Coldfusion 9.00 because CF 9.01 broke too many of my cfgrids as commented by many at the end of this post:
http://www.coldfusionjedi.c...
However, for this autocomplete to work with a remote datasource using a cfc, one need to be at CF 9.01 because 9.00 doesn't work.
It's kind of a hard choice when you need the grids to be working and you need to integrate some advanced autocomplete features at the same time.
More than a year after 9.01's release they still haven't fixed that... Well...Time to move to Jquery or something else for the grids.
As usual, thanks Ray for the really good stuff you put on here. It is really helpful.
Cheers
What would be the best way to force users to select from the JSON and not be able to enter whatever they want?
I'm using this to search over 5k employees.
This does what I was looking for:
//require that users select from the list and not type their own entry. usage:(class="autocomplete")
$('.autocomplete').autocomplete({
change: function( event, ui ) {
if ( !ui.item ) {
$(this).val('');
}
}
});
Glad you got it. Was on the road all day yesterday.
Hi Ray, great tutorial. I actually just coded the exact same thing today in PHP and the end result really produces a "wow".
However! What do you think about all those requests that have to happen? Each image is a request made to gravatar, which although fast, still represents a problem on an autocomplete that should be as responsive as possible.
I'm trying to think of a way of maybe caching the resulting images? I don't think that would work, unless I made a job that would cache each user's gravatar (in db or memory) and then the response would including it in the src as Base64 encoded data.
Well, I'd imagine the 2nd request for gravatar X would be cached, wouldn't it?
Well, Mike's right. The client machines will make a ton of remote hit requests. Gravatar is pretty darn fast (probably a cdn), so I wouldn't worry about it here.
But for argument's sake (for scenarios where it either might not be fast - say it wasn't gravatar, or your users are behind a firewall where they can't access the remote data) what I'd do is maybe have the image tags src attribute value refer to a .cfm file on the server (or in your case, a .php file) with the url variables needed. Then you have the cfm file do a conditional statement: do I have the cached data? If yes, show the stored image from my server, if not, go to the remote site, store the image in a directory and store a key:value pair of data in a cached variable that has information of the hash and maybe image filename (or something to that affect - I don't recall offhand what gravatar sends... it's been a couple years since I played with it). Either way, it should be pretty simple to do. You can cache the data in either the app scope, ehcache, ram disk, etc (I'm not sure what options PHP has for caching, but I'm assuming they have something similar). You'll also want to consider what happens when you clear the cache (ie. restart CF), so storing the data in a DB as well might not be a bad idea (then on app init you can create the cache again from the DB). Anyway, food for thought :).
Regarding the firewall example above: since the cf server is the one making the request to get the image, I'd have the firewall grant access to the CF server to get the remote images and store them where the user/client machines can view them from the web server.
Jeff's onto what I was talking about. The firewall is an issue I hadn't even considered yet, but could definitely be a concern in my case. I was mainly worried about # of requests.
I might be wrong, but I think your solution doesn't necessarily reduce the number of requests, it just routes them to my server instead. This wouldn't greatly improve performance on the user-end. I think what you have proposed is probably what Gravatar's coders have done on their end to make responses really snappy.
I have tested this out with Gravatar and if I limit my results to ~25 users, I barely notice a hitch loading gravatars vs. just plain text.
One optimization that can be done is to store the md5'd version of the email address in the database ... if I'm not mistaken md5 can take up a lot of cycles to compute and that precomputation could speed up performance.
> I might be wrong, but I think your solution doesn't necessarily reduce the number of requests, it just routes them to my server instead. This wouldn't greatly improve performance on the user-end.
---
Lol. They are separate images... so, yes, they are separate requests from the client to the server. Unless you know of some magic I don't know about :).
I suppose you could somehow melt them all into one long teaser image so that it's only one request, but you'd have to create one for every search term, so thats not going to happen :).
Other than that, I'm not sure where you're going with this now. They are images. And images require requests from the client machine to a server (even if you check for a cached variable, you're not going to see a degradation in speed here. Solutions like EHCache are so darn fast its not even an argument worth discussing). If the host server wasn't a fast cdn (like gravatar) and prone to slowness then a caching solution is the way to go. Besides it was just an idea off the top of my head.
Another you could do is just have CF grab all the gravatar images on applicaiton init (since their relative email addresses are already in your database). Then have all the client machines just always look for an image file on the local server. This would eliminate the cached variable (which I still don't think will be slow in any way - I was just having fun by throwing that idea out there), but you still run into the problem (?) of the clients requesting all those images from the local server (still not sure how you plan to get around that since thats the way the web works :).
I also forgot to mention another caching option yesterday... a proxy cache (like varnish cache). Overkill? yes, but just throwing fun ideas out there. But even though that would be super fast as well, you'd still be making separate requests from the client machine to the varnish cache server for each image.
A caching solution would be fast and not a problem for anyone. So where are you planning to go with this? Why would it suddenly be bad for the client machine to request all those images even if its from the local server? I mean, it's the way the whole web is run :). You put an image on the page, and the client has to make a request for it.
Heh. Its most likely I completely missed your point in which case ignore half of the above :)
Hey Jeff, I wasn't shooting down your answer. I recognize it makes technical sense, but I was just saying that it doesn't solve the original problem I was asking about.
For example, my original question posed a possible solution to "the way the web works", and that was to "fetch" (whether via filesystem, cache, or otherwise) the images server side and insert them as Base64 encoded data right into the json response. Thereby only having 1 request (the original AJAX autocomplete). Check out this article to see what I'm talking about: http://www.stoimen.com/blog....
That said, in modern browsers having another ~20 image requests after the page has loaded happens pretty damn fast. Not to mention that if you return the text ASAP, then even if the image hasn't loaded yet - the user can still select a name.
I might try benchmarking these two solutions and see what holds up under load.
thanks
Hi Ray, just want to say thank you for this useful post. Although I'm not using jQuery-UI I was able to use the example in this tutorial to get it working with https://github.com/devbridg....
Glad this old one is still useful. :)
Hi Ray, I find this search method extremely useful. If I have a database of ~30K records and would like to search for make, model and year for example and display a picture of the automobile, can this be accomplished? How would I go about building such an autocomplete search? Thanks!
Pretty much the exact same - you just want to be sure you limit the possible matches. If I search for "e" you dont want to return 20K results.