jQuery Plugin: Auto Complete v3.0

July 26, 2009

[UPDATE] Auto-complete project has been revamped, please check out it's homepage for details.




Auto-complete2.1 is a nice plugin but it required too much setup for my liking. So i went ahead and ran a big overhaul to the function to make it more powerful and event driven. It's still a measly 173 lines of code (30 of which are just settings), but I've implemented a much better system within it.

To start off, there is no need for a certain html layout for it to work correctly. The function attaches itself to input fields instead of the parent DIV's. It also creates the UL drop down from within so you don't have to worry about it.

I've added metadata support so you can have one standard init call that will latch onto all input's specified, and then set differen't mini params for each input seperately. In the example package, I use the standard class attribute to hold my extra data, but you can store it anyway you like.

All data returned from the server script is now stored as part of the LI element using the data method. So whole objects can be stored on each element seperately.

Event callbacks are now added for onSelect, onBlur, onRollover, and onMaxRequests. All but the onMaxRequests callbacks are sent two parameters: (data, $li). The data var contains the data returned from the server script as an object. The $li var is the jQuery object of the LI element in relation.

Script level caching is now implemented, so that the same request doesn't get made twice. Initial limit on the number of cached objects is set to 50. Once reached, the cache is flushed and you start over. You should adjust accordingly.

And finally, you can customize the POST parameters sent to the server with the postData option. Just note that the postVar entry will always be overridden with the users input. Let's take a look at a simple example:

$(document).ready(function(){
	$('input[name=auto-complete]').autoComplete();
});

I've included the docs page in the example package below the source code, as well as a few more examples on how to use this plugin. Be sure to check them out.

/**
 * Auto Complete v3.0
 * July 26, 2009
 * Released under the MIT License @ http://www.codenothing.com/license
 * Corey Hart @ http://www.codenothing.com
 */ 
;(function($){
	$.fn.autoComplete = function(options){
		// Set up autocomplete on all possible elements
		return this.each(function(){
			// Cache objects
			var $input = $(this).attr('autocomplete', 'off'), $li, timeid, blurid, 
				// Set defaults and include metadata support
				settings = $.extend({
					// Inner Function Defaults (Best to leave alone)
					opt: -1,
					inputval: '',
					mouseClick: false,
					// Server Script Path
					ajax: 'ajax.php',
					// Drop List CSS
					list: 'auto-complete-list',
					rollover: 'auto-complete-list-rollover',
					width: $input.outerWidth(),
					top: $input.offset().top + $input.outerHeight(),
					left: $input.offset().left,
					// Post Data
					postVar: 'value',
					postData: {},
					// Limitations
					minChars: 1,
					maxRequests: 0,
					requests: 0, // Inner Function Default
					// Events
					onMaxRequest: function(){},
					onSelect: function(){},
					onRollover: function(){},
					onBlur: function(){},
					preventEnterSubmit: false,
					enter: false, // Inner Function Default
					delay: 100,
					selectFuncFire: true, // Inner Function Default
					// Caching Options
					useCache: true,
					cacheLimit: 50,
					cacheLength: 0, // Inner Function Default
					cache: {} // Inner Function Default
				}, options||{}, $.metadata?$input.metadata():{});

			// Create the drop list (Use an existing one if possible)
			var $ul = $('ul.'+settings.list)[0] ? $('ul.'+settings.list) : $('<ul/>').appendTo('body').addClass(settings.list).hide();

			// Run on keyup
			$input.keyup(function(e){
				var key = e.keyCode;
				settings.enter = false;
				settings.mouseClick = false;
				if ((key > 47 && key < 91) || key == 8){ // Input Keys and Backspace
					settings.opt = -1;
					settings.inputval = $input.val();
					if (settings.inputval.length >= settings.minChars){
						if (timeid) clearTimeout(timeid);
						timeid = setTimeout(function(){ sendRequest(settings); }, settings.delay);
					} else if (key == 8) { // Remove list on backspace of small string
						$ul.html('').hide();
					}
				}
				else if (key == 13 && $li){ // Enter
					settings.opt = -1;
					settings.enter = true;
					if (settings.selectFuncFire) {
						settings.selectFuncFire = false;
						settings.onSelect($li.data('data'), $li);
						setTimeout(function(){ settings.selectFuncFire = true; }, 1000);
					}
					$ul.hide();
				}
				else if (key == 38){ // Up arrow
					if (settings.opt > 0){
						settings.opt--;
						$li = $('li', $ul).removeClass(settings.rollover).eq(settings.opt).addClass(settings.rollover);
						$input.val($li.data('data').value||'');
						settings.onRollover($li.data('data'), $li);
					}else{
						settings.opt = -1;
						$input.val(settings.inputval);
						$ul.hide();
					}
				}
				else if (key == 40){ // Down arrow
					if (settings.opt < $('li', $ul).length-1){
						settings.opt++;
						$li = $('li', $ul.show()).removeClass(settings.rollover).eq(settings.opt).addClass(settings.rollover);
						$input.val($li.data('data').value||'');
						settings.onRollover($li.data('data'), $li);
					}
				}
			}).blur(function(){
				blurid = setTimeout(function(){
					if (settings.mouseClick) return false;
					settings.opt = -1;
					settings.onBlur(settings.inputval, $ul);
					$ul.hide();
				}, 150);
			}).parents('form').eq(0).submit(function(){
				return settings.preventEnterSubmit ? settings.enter : true;
			});
	
			// Ajax/Cache Request
			function sendRequest(settings){
				// Check Max reqests first
				if (settings.maxRequests && settings.requests > settings.maxRequests){
					return settings.onMaxRequest();
				}else if (settings.maxRequests)
					settings.requests++;

				// Load from cache if possible
				if (settings.useCache && settings.cache[settings.inputval])
					return loadResults(settings.cache[settings.inputval]);

				// Send request server side
				settings.postData[settings.postVar] = settings.inputval
				$.post(settings.ajax, settings.postData, function(json){
					json = json && json != '' ? eval('('+json+')') : {};
					// Store results into the cache if need be
					if (settings.useCache){
						settings.cacheLength++;
						settings.cache[settings.inputval] = json;
						// Shift out old cache if necessary
						if (settings.cacheLength > settings.cacheLimit){
							settings.cache = {};
							settings.cacheLength = 0;
						}
					}
					// Show the list if there is a return, else hide it
					if (json.length > 0)
						loadResults(json);
					else
						$ul.html('').hide();
				});
			}

			// List Functionality
			function loadResults(list){
				// Clear the List and align it properly
				$ul.html('').css({top: settings.top, left: settings.left, width: settings.width});
				// Add new rows to the list
				for (i in list)
					if (list[i].value) $('<li/>').appendTo($ul).html(list[i].display||list[i].value).data('data', list[i]);
				// Start mouse actions after list is set and shown
				$ul.show().children('li').mouseover(function(){
					$li = $(this);
					$('li.'+settings.rollover, $ul).removeClass(settings.rollover);
					$input.val( $li.addClass(settings.rollover).data('data').value );
					settings.onRollover($li.data('data'), $li);
				}).click(function(){
					settings.mouseClick = true;
					if (blurid) clearTimeout(blurid);
					settings.onSelect($li.data('data'), $li);
					$ul.hide();
					// Bring the focus back to the input when clicking a list member
					$input.focus();
				});
	
				// Return orignal val when not hovering
				$ul.mouseout(function(){
					$('li.'+settings.rollover, $ul).removeClass(settings.rollover);
					if (! settings.mouseClick) $input.val(settings.inputval);
				});
			}
		});
	};
})(jQuery);
Have a question or comment? ask@codenothing.com.