jQuery multi-source auto-suggest

This post is about a jQuery auto-suggest plugin I developed that can look up multiple sources and return data.

The source code

Here’s the full source code for the plugin as well as how to call it.

  1. jQuery Auto-suggest plugin
  2. Auto Suggest Calling Script

Tom Coote’s excellent plugin was the basis for this one. However, what I needed was a plugin that could look up multiple sources, synchronise the returned data and then display it. The application for this was our intranet, which required an auto-suggest feature showing a-z content as well as internal contacts.

Intranet Auto Suggest SearchThe difficulty here was that the data sources were in different formats. The a-z data was coming from a flat xml file, but the phonebook data was coming from a web service returning xml. So, the plugin naturally retrieved the flat file quicker than the web service. Also, the schema for each data source was different and the desired output was different. The a-z search needed to just show a link whereas the phonebook search needed to show the persons name, role, and provide links to email and phone (using microsoft lync).

I needed a way that would allow me to call as many data sources as I wanted, specify how the data should be handled when it was returned, and synchronise the output so it appeared all at once.

Defining search objects

In the calling file, an array of search objects is passed to the jquery plugin. Each of these define a number of things. Take the a-z search as an example…

//...
searches: [{
   id: 'autosuggest-atoz-results',
   url: HOST + '/EasySiteWeb/EasySite/StyleData/Interface_Master/Includes/interface-atoz.xml',
   title: 'A-Z Results',
   dataFilter: 'records record', //1
   cacheData: true, //2
   getSearchField: function () { //3
      return $(this).find('data[field="Link Text"]').text();
   },
   getListItem: function (highlightMatches, filterPatt) { //4
      var obj = $(this),
          label = obj.find('data[field="Link Text"]').text(),
          text = highlightMatches === true ? label.replace(filterPatt, '$1') : label,
          url = obj.find('data[field="Link Url"]').text(),
          targetWindow = parseInt(obj.find('data[field="Link Open in New Window"]').text()) === 1 ? "_blank" : "_self";
      return $('' + text + '');
   },
   getDisplayText: function () { 
      return $(this).find('data[field="Link Text"]').text();
   }
}, 
//...

Going through each of the numbered sections in the above code…

  1. dataFilter – Once the data is retrieved, this is an xpath expression to determine where to get each node in the xml
  2. cacheData – for flat files, there is no point doing a data look-up on each keypress, so they are cached to reduce overheads.
  3. getSearchField – this defines a function used to determine which field in the xml data should be used as the source for the auto-suggest.
  4. getListItem – once the autosuggest has been run, this function determines how to output the list item to be displayed. Since the plugin can look up multiple sources, each of these search objects must define its own function to display the list item.

Auto-suggesting stuff

Now to the jQuery plugin. The actual auto-suggest is pretty similar to most others. There are a few interesting bits though.

Within the keyListener function, unless the key pressed was enter, left, right, up or down, the ‘runSuggest’ function will run.

RunSuggest

This function checks whether the minimum characters threshold has been reached. By default, auto-suggest will not start running unless more than 2 characters have been pressed. The getRequest function is called which clears and starts a timer. This is so that subsequent key presses will cancel out the getRequest function unless the timer limit is reached. The effect is that once the user stops typing the word, the search will then be run. It shouldn’t be run every time a key is pressed.

The bit that handles the multi-source request is now executed.

$.when.apply($, getCommands()).done(function () { //...

This line uses javascripts ‘apply’ function to carry out the multiple data requests, waits until data has been returned (using the ‘done’ function), and then processes the resulting data. The ‘getCommands’ function is shown below…

function getCommands() {
  var commands = $.map(settings.searches, function (s) {
    if (s.cacheData && s.data && s.data.length) {
      return s.data;
    }
    return $.get(s.url, { search: text, max_records: settings.maxResults }, "xml");
  });
  return commands;
}

This function uses jquery’s ‘map’ function to create an array of ajax ‘get’ commands. The url’s of these ajax commands are defined in the calling scripts search object array. A check is made first, to see if the data was meant to be cached. If it was and the data is available, that data is simply returned, rather than doing an ajax call. Otherwise, the ajax call is made using the text that the user typed in, the max results to return and then format the data should be returned in.

The ‘done’ function above will wait until all ajax requests have completed and returned a response before executing. This is how the data is synchronised.

Inside the ‘done’ function, the specific bits of information from the actual data is retrieved…

$(settings.searches).each(function (i) {
  //if the data has just been loaded
  if (!(this.cacheData && this.data && this.data.length)) {
    if (response[i][2].responseXML) {
      this.data = $(response[i][2].responseXML).find(this.dataFilter);
    }
  } else {
    this.data = response[i];
  }
  if (this.data) {
    search.apply(obj, [this]);
    $(results).append(this.results);
  }
});

A loop runs through each search object defined in the calling script. Another check is made to see whether the data was cached and if it is available. If the data is there, the xml is processed using the ‘dataFilter’ function again specified in the calling script.

Finally, if the data wasn’t meant to be cached and is available, the data is searched using the ‘search’ function and the results are appended to the html container.

The search function uses the text that the user entered and match it against the xml node according to the function passed from the calling script.

if (filterPatt.test(searchPart.getSearchField.apply(data[i])) === bMatch) {
  resultObjects.push(data[i]);
  iFound += 1;
}

The searchPart object is the one defined in the calling script and contains a function called ‘getSearchField.’ This function specifies how to match a result against the user input. In the case of the a-z search, this is the following…

getSearchField: function () {
  return $(this).find('data[field="Link Text"]').text();
}

So, an xml node is passed to this function, again using the ‘apply’ function. Within the ‘getSearchField’ function, ‘this’ then becomes the xml object. This function then specifies the text to match against the user input is defined as belonging to to the ‘data’ node and the ‘Link Text’ attribute of that node.

Future stuff

The plugin could quite easily be extended to handle json data since the way that searching ajax-returned data is defined is in the calling script, not in the plugin. The plugin just expects a callback function with a specific name so it can be called at the appropriate time.

Thanks again to Tom Coote for his excellent plugin, on which this is based. Any constructive comments welcome.

 

Leave a Reply