Introduction
This widget extends the functionalities of jQueryUI Autocomplete widget by adding infinite scrolling when loading data from a remote source.
You can find the original article on my blog.
Pre-requisites
- JavaScript, jQuery, jQueryUI
- ASP.NET
Background
Let’s assume we are given a project that uses the Autocomplete Widget and the number of results given on a key press is so high that is affects the responsiveness of the application.
We want to be able to load only a few items initially and for every other request, load only a given number of items. So we will load the first N items and when the user reaches the last item (by scrolling to it or continually pressing key down until the last item is reached), we make another request to the server to load the next N items.
Using the Code
Let’s say we have an input:
<input id="inp" style="width: 350px;" type="text" />
and the widget implementation:
$("#inp").autocomplete(options);
For more details on this code, read more at jQueryUI Autocomplete documentation website.
We want to make as few changes as possible to the code to get the desired effect of infinite scrolling. The new code would look like this:
$("#inp").smartautocomplete({
getDataFunc: getData,
pageSize: 15
...
});
For the purpose of this example, I have used an ASP.NET application that exposes a WebMethod
that returns some dummy data to use with our input. The function looks like this:
var getData = function (input, pageIndex, pageSize, callback) {
PageMethods.GetData(input, pageIndex, pageSize, function (response) {
if (response) {
response = $.map(response, function (item) {
return {
label: item,
value: item
}});
callback(response);
}
else callback();
});
}
As seen above, we have a source to get our data from. It can be something as a web service or anything else for a matter of fact.
The Code
We would have to create a wrapper widget that has the original autocomplete widget underneath, but handles a little bit different the data loading logic. We start by creating a jQuery plugin as instructed by jQuery plugins authoring documentation. The new widget would take two new parameters:
pageSize
: Number of items to load on every request getDataFunc
: Function to be called that returns our data from the server
We overwrite option.source
parameter with our own function that does the magic:
options.source = function (request, response) {
...
options.getDataFunc(term, pageIndex + 1, options.pageSize, function (r) {
if (r) {
if (!r.length)
startedLoadingFromScroll = false;
if (data.length)
for (var i = 0; i < r.length; i++)
data.push(r[i]);
else data = r;
response(data);
autocomplete.scrollTop(lastScrollTop);
pageIndex++;
}
});
...
};
The next thing we do is to bind to several events triggered by the autocomplete widget. We first bind to autocompletecreate
event to implement our own functionality just after the autocomplete widget has been created. Here we bind to the scroll event of the autocomplete dropdown to handle the case when we reach the end of the list. We achieve this by constantly verifying the following inequality:
autocomplete[0].scrollHeight - autocomplete.scrollTop()
The result of this is true
only when we have scrolled to the end of the list. If this is the case, we call our startSearching
function.
.bind('autocompletecreate', function () {
autocomplete = $(this).autocomplete('widget');
menu = parent.data().autocomplete.menu.element;
if (autocomplete.attr('sa-scroll') != 'on') {
autocomplete.scroll(function (e) {
if (loading)
return stopEvent(e);
if (startedLoadingFromScroll) {
if ($.browser.msie || $.browser.mozilla)
autocomplete.scrollTop(lastScrollTop);
return stopEvent(e);
}
if (autocomplete[0].scrollHeight - autocomplete.scrollTop()
Next we bind to autocompletefocus
. This is called every time we navigate with the keyboard through the dropdown items. By default, autocomplete gives the focused element the class ui-state-hover. So, by verifying autocomplete.find(".ui-menu-item:last .ui-state-hover").length == 1
, we are certain* the last item has focused. This is the other case when we need to load more data.
.bind('autocompletefocus', function (event, ui) {
focusedItem = ui.item;
if (ignoreFocus) {
ignoreFocus = false;
return;
}
if (autocomplete.find(".ui-menu-item:last .ui-state-hover").length)
startSearching();
})
*jQueryUI has a certain way to render the list items. If we change the way items are rendered, we have to reconsider this verification.
We then bind to autocompleteopen
to set focus to the last focused item before the dropdown closed.
.bind('autocompleteopen', function (event, ui) {
if (!selectedIndex) {
if (options.autoFocus) {
ignoreFocus = true;
menu.menu('activate', event, autocomplete.find(".ui-menu-item:first"));
}
}
else {
ignoreFocus = true;
menu.menu('activate', event, autocomplete.find
(".ui-menu-item:eq(" + selectedIndex + ")"));
}
})
The last thing we do is bind to autocompleteclose
to reset our search data.
.bind('autocompleteclose', function (event, ui) {
resetData();
})
Things to Consider for Future Development
- This widget takes advantage of the still unpublished Menu Widget. Therefore any change in its functionality will directly affect our functionality.
- We base our algorithm of finding the focused item on the premise that it has the class ui-state-hover. This may not be good when customizing search results rendering system.
- Paging for local sources should be implemented.
History
- 4th February, 2012: Initial version
- 22nd February, 2012: Updated source code