How to implement a custom autocomplete search component

The XUL textbox element supports an autocomplete mechanism that is used to create a textbox with a popup containing a list of possible completions for what the user has started to type. There are actually two autocomplete mechanisms:

  • An older mechanism that is part of the "xpfe" codebase and is used in older applications such as Thunderbird and SeaMonkey.
  • A newer and slightly simplified mechanism that is used in "toolkit" applications such as Firefox and XULRunner.
  • Thunderbird 2.x and SeaMonkey 1.1.x support the toolkit interfaces, although they do not use the same autocomplete widget.

To use autocomplete with a XUL textbox, all you need to do is set some attributes:

<textbox type="autocomplete" autocompletesearch="history"/>

The type="autocomplete" turns on the autocomplete mechanism and the autocompletesearch="history" sets the source for the autocomplete. There are more options and features, see XUL:textbox_(Firefox_autocomplete) for more details. The toolkit mechanism has built-in support for several autocomplete sources, including:

  • history: Search the browser's URL history (Firefox: 1.0+; SeaMonkey: 1.1+)
  • form-history: Search the values that the user has entered into form fields. (Firefox: 1.0+; SeaMonkey: 2.0+)
  • file: Search file names (Firefox: 2.0+; Thunderbird: 2.0+; SeaMonkey: 1.1+; GTK builds only). The component uses the autocompletesearchparam attribute or searchParam property to allow the developer to define the default directory otherwise only paths beginning with / or ~/ will be autocompleted.

Building your custom autocomplete search components

Sometimes, you may want to build your own autocomplete source. To do so you need the following:

  • Create an XPCOM component that implements the nsIAutoCompleteSearch interface.
  • Make sure the contract ID of your XPCOM component follows the following form: "@mozilla.org/autocomplete/search;1?name=xxx", where "xxx" is the name of your autocomplete source.
  • Implement the list of matched strings using the nsIAutoCompleteResult interface.

The simplest way to make an XPCOM component is to build an XPCOM JavaScript Component (this cannot be done with a JavaScript module). How to Build an XPCOM Component in Javascript will step you through the process. Because creating an XPCOM component in order to build a custom autocomplete source can be enough to discourage some developers, below is an example JavaScript XPCOM component called "simple-autocomplete" that implements the necessary interfaces.

Note: When copying the examples below, change the UUID that uniquely identifies the component, otherwise you might have problems if your component gets installed along another component derived from those same examples. That can happen! So, get a UUID from http://www.famkruithof.net/uuid/uuidgen, for example, and paste it in.

Basic example for Gecko 2.0 and up (Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1)

This example is your first best try because:

  • it has no specific logic (it just returns a dummy array of choices)
  • it doesn't care about compatibility with older Gecko versions

First copy the following JavaScript code into a file named basic_autocomplete.js into the components directory (or whatever components folder is appropriate in your case):

Warning: The UUID used below in chrome.manifest and assigned to CLASS_ID must be changed before use.

const Ci = Components.interfaces;
const Cu = Components.utils;

Cu.import('resource://gre/modules/XPCOMUtils.jsm');

const CLASS_ID = Components.ID('x753d830-ba1e-11e0-962b-0800200c9a66'); // ← Change this
const CLASS_NAME = "Basic AutoComplete";
const CONTRACT_ID = '@mozilla.org/autocomplete/search;1?name=basic-autocomplete';

/**
 * @constructor
 *
 * @implements {nsIAutoCompleteResult}
 *
 * @param {string} searchString
 * @param {number} searchResult
 * @param {number} defaultIndex
 * @param {string} errorDescription
 * @param {Array.<string>} results
 * @param {Array.<string>|null=} comments
 */
function ProviderAutoCompleteResult(searchString, searchResult,
  defaultIndex, errorDescription, results, comments) {
  this._searchString = searchString;
  this._searchResult = searchResult;
  this._defaultIndex = defaultIndex;
  this._errorDescription = errorDescription;
  this._results = results;
  this._comments = comments;
}

ProviderAutoCompleteResult.prototype = {
  _searchString: "",
  _searchResult: 0,
  _defaultIndex: 0,
  _errorDescription: "",
  _results: [],
  _comments: [],

  /**
   * @return {string} the original search string
   */
  get searchString() {
    return this._searchString;
  },

  /**
   * @return {number} the result code of this result object, either:
   *   RESULT_IGNORED   (invalid searchString)
   *   RESULT_FAILURE   (failure)
   *   RESULT_NOMATCH   (no matches found)
   *   RESULT_SUCCESS   (matches found)
   */
  get searchResult() {
    return this._searchResult;
  },

  /**
   * @return {number} the index of the default item that should be entered if
   *   none is selected
   */
  get defaultIndex() {
    return this._defaultIndex;
  },

  /**
   * @return {string} description of the cause of a search failure
   */
  get errorDescription() {
    return this._errorDescription;
  },

  /**
   * @return {number} the number of matches
   */
  get matchCount() {
    return this._results.length;
  },

  /**
   * @return {string} the value of the result at the given index
   */
  getValueAt: function(index) {
    return this._results[index];
  },

  /**
   * @return {string} the comment of the result at the given index
   */
  getCommentAt: function(index) {
    if (this._comments)
      return this._comments[index];
    else
      return '';
  },

  /**
   * @return {string} the style hint for the result at the given index
   */
  getStyleAt: function(index) {
    if (!this._comments || !this._comments[index])
      return null;  // not a category label, so no special styling

    if (index == 0)
      return 'suggestfirst';  // category label on first line of results

    return 'suggesthint';   // category label on any other line of results
  },

  /**
   * Gets the image for the result at the given index
   *
   * @return {string} the URI to the image to display
   */
  getImageAt : function (index) {
    return '';
  },

  /**
   * Get the final value that should be completed when the user confirms
   * the match at the given index.
   * @return {string} the final value of the result at the given index
   */
  getFinalCompleteValueAt: function(index) {
    return this.getValueAt(index);
  },

  /**
   * Removes the value at the given index from the autocomplete results.
   * If removeFromDb is set to true, the value should be removed from
   * persistent storage as well.
   */
  removeValueAt: function(index, removeFromDb) {
    this._results.splice(index, 1);

    if (this._comments)
      this._comments.splice(index, 1);
  },

  getLabelAt: function(index) { return this._results[index]; },

  QueryInterface: XPCOMUtils.generateQI([ Ci.nsIAutoCompleteResult ])
};


/**
 * @constructor
 *
 * @implements {nsIAutoCompleteSearch}
 */
function ProviderAutoCompleteSearch() {
}

ProviderAutoCompleteSearch.prototype = {

  classID: CLASS_ID,
  classDescription : CLASS_NAME,
  contractID : CONTRACT_ID,

  /**
   * Searches for a given string and notifies a listener (either synchronously
   * or asynchronously) of the result
   *
   * @param searchString the string to search for
   * @param searchParam an extra parameter
   * @param previousResult a previous result to use for faster searchinig
   * @param listener the listener to notify when the search is complete
   */
  startSearch: function(searchString, searchParam, previousResult, listener) {
    var results = ['Mary', 'John'];
    var autocomplete_result = new ProviderAutoCompleteResult(searchString,
        Ci.nsIAutoCompleteResult.RESULT_SUCCESS, 0, "", results, null);

    listener.onSearchResult(this, autocomplete_result);
  },

  /**
   * Stops an asynchronous search that is in progress
   */
  stopSearch: function() {
  },

  QueryInterface: XPCOMUtils.generateQI([ Ci.nsIAutoCompleteSearch ])
};

// The following line is what XPCOM uses to create components
const NSGetFactory =
  XPCOMUtils.generateNSGetFactory([ ProviderAutoCompleteSearch ]);



Then register your XPCOM component. To do so put the following declaration in your chrome.manifest file:

component {x753d830-ba1e-11e0-962b-0800200c9a66} components/basic_autocomplete.js
contract @mozilla.org/autocomplete/search;1?name=basic-autocomplete {x753d830-ba1e-11e0-962b-0800200c9a66}

And finally use this newly available component in a XUL file like this:

<textbox id="text1" type="autocomplete" autocompletesearch="basic-autocomplete"/>

That's it!

A bit more complex example compatible with old Gecko versions

First copy the following JavaScript code into a file named "simple_autocomplete.js" into the "components" directory (or whatever "components" folder is appropriate in your case)

Warning: The UUID used below in chrome.manifest and assigned to CLASS_ID must be changed before use.

const Ci = Components.interfaces;

const CLASS_ID = Components.ID("6224daa1-71x2-4d1a-ad90-01ca1c08e323"); // ← Change this
const CLASS_NAME = "Simple AutoComplete";
const CONTRACT_ID = "@mozilla.org/autocomplete/search;1?name=simple-autocomplete";

// Implements nsIAutoCompleteResult
function SimpleAutoCompleteResult(searchString, searchResult,
                                  defaultIndex, errorDescription,
                                  results, comments) {
  this._searchString = searchString;
  this._searchResult = searchResult;
  this._defaultIndex = defaultIndex;
  this._errorDescription = errorDescription;
  this._results = results;
  this._comments = comments;
}

SimpleAutoCompleteResult.prototype = {
  _searchString: "",
  _searchResult: 0,
  _defaultIndex: 0,
  _errorDescription: "",
  _results: [],
  _comments: [],

  /**
   * The original search string
   */
  get searchString() {
    return this._searchString;
  },

  /**
   * The result code of this result object, either:
   *         RESULT_IGNORED   (invalid searchString)
   *         RESULT_FAILURE   (failure)
   *         RESULT_NOMATCH   (no matches found)
   *         RESULT_SUCCESS   (matches found)
   */
  get searchResult() {
    return this._searchResult;
  },

  /**
   * Index of the default item that should be entered if none is selected
   */
  get defaultIndex() {
    return this._defaultIndex;
  },

  /**
   * A string describing the cause of a search failure
   */
  get errorDescription() {
    return this._errorDescription;
  },

  /**
   * The number of matches
   */
  get matchCount() {
    return this._results.length;
  },

  /**
   * Get the value of the result at the given index
   */
  getValueAt: function(index) {
    return this._results[index];
  },

  /**
   * Get the comment of the result at the given index
   */
  getCommentAt: function(index) {
    return this._comments[index];
  },

  /**
   * Get the style hint for the result at the given index
   */
  getStyleAt: function(index) {
    if (!this._comments[index])
      return null;  // not a category label, so no special styling

    if (index == 0)
      return "suggestfirst";  // category label on first line of results

    return "suggesthint";   // category label on any other line of results
  },

  /**
   * Get the image for the result at the given index
   * The return value is expected to be an URI to the image to display
   */
  getImageAt : function (index) {
    return "";
  },

  /**
   * Get the final value that should be completed when the user confirms
   * the match at the given index.
   * @return {string} the final value of the result at the given index
   */
  getFinalCompleteValueAt: function(index) {
    return this.getValueAt(index);
  },

  /**
   * Remove the value at the given index from the autocomplete results.
   * If removeFromDb is set to true, the value should be removed from
   * persistent storage as well.
   */
  removeValueAt: function(index, removeFromDb) {
    this._results.splice(index, 1);
    this._comments.splice(index, 1);
  },

  QueryInterface: function(aIID) {
    if (!aIID.equals(Ci.nsIAutoCompleteResult) && !aIID.equals(Ci.nsISupports))
        throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};


// Implements nsIAutoCompleteSearch
function SimpleAutoCompleteSearch() {
}

SimpleAutoCompleteSearch.prototype = {
  /*
   * Search for a given string and notify a listener (either synchronously
   * or asynchronously) of the result
   *
   * @param searchString - The string to search for
   * @param searchParam - An extra parameter
   * @param previousResult - A previous result to use for faster searchinig
   * @param listener - A listener to notify when the search is complete
   */
  startSearch: function(searchString, searchParam, result, listener) {
    // This autocomplete source assumes the developer attached a JSON string
    // to the the "autocompletesearchparam" attribute or "searchParam" property
    // of the <textbox> element. The JSON is converted into an array and used
    // as the source of match data. Any values that match the search string
    // are moved into temporary arrays and passed to the AutoCompleteResult
    if (searchParam.length > 0) {
      var nativeJSON = Components.classes["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
      var searchResults = nativeJSON.decode(searchParam);
      // These 2 lines above are for Thunderbird < v7
      // Since Thunderbird 3, they can be replaced more efficiently by the following line:
      // var searchResults = JSON.parse(searchParam);

      var results = [];
      var comments = [];
      for (i=0; i<searchResults.length; i++) {
        if (searchResults[i].value.indexOf(searchString) == 0) {
          results.push(searchResults[i].value);
          if (searchResults[i].comment)
            comments.push(searchResults[i].comment);
          else
            comments.push(null);
        }
      }
      var newResult = new SimpleAutoCompleteResult(searchString, Ci.nsIAutoCompleteResult.RESULT_SUCCESS, 0, "", results, comments);
      listener.onSearchResult(this, newResult);
    }
  },

  /*
   * Stop an asynchronous search that is in progress
   */
  stopSearch: function() {
  },

  QueryInterface: function(aIID) {
    if (!aIID.equals(Ci.nsIAutoCompleteSearch) && !aIID.equals(Ci.nsISupports))
        throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

// Factory
var SimpleAutoCompleteSearchFactory = {
  singleton: null,
  createInstance: function (aOuter, aIID) {
    if (aOuter != null)
      throw Components.results.NS_ERROR_NO_AGGREGATION;
    if (this.singleton == null)
      this.singleton = new SimpleAutoCompleteSearch();
    return this.singleton.QueryInterface(aIID);
  }
};

// Module
var SimpleAutoCompleteSearchModule = {
  registerSelf: function(aCompMgr, aFileSpec, aLocation, aType) {
    aCompMgr = aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
    aCompMgr.registerFactoryLocation(CLASS_ID, CLASS_NAME, CONTRACT_ID, aFileSpec, aLocation, aType);
  },

  unregisterSelf: function(aCompMgr, aLocation, aType) {
    aCompMgr = aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
    aCompMgr.unregisterFactoryLocation(CLASS_ID, aLocation);
  },

  getClassObject: function(aCompMgr, aCID, aIID) {
    if (!aIID.equals(Components.interfaces.nsIFactory))
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;

    if (aCID.equals(CLASS_ID))
      return SimpleAutoCompleteSearchFactory;

    throw Components.results.NS_ERROR_NO_INTERFACE;
  },

  canUnload: function(aCompMgr) { return true; }
};

// Module initialization
function NSGetModule(aCompMgr, aFileSpec) { return SimpleAutoCompleteSearchModule; }


Starting in Gecko 2.0, component registration has been changed, so you need to make the following changes:

  1. Add an NSGetFactory() function:

    function NSGetFactory(cid) {
    if (cid.toString().toUpperCase() != CLASS_ID.toString().toUpperCase()) {
    throw Components.results.NS_ERROR_FACTORY_NOT_REGISTERED;
    }

    return SimpleAutoCompleteSearchFactory;
    }
  2. You need to explicitly register the component by adding these lines into your chrome.manifest file:

    component {6224daa1-71x2-4d1a-ad90-01ca1c08e323} components/.js
    contract @mozilla.org/autocomplete/search;1?name=simple-autocomplete {6224daa1-71x2-4d1a-ad90-01ca1c08e323}
  3. You need to add the following method:
    getLabelAt: function(index) { return this._results[index]; }
    to
    SimpleAutoCompleteResult

Use this newly available component in a XUL file like this:

<textbox id="text1" type="autocomplete" autocompletesearch="simple-autocomplete" showcommentcolumn="true"
         autocompletesearchparam='[{"value":"mark","comment":"cool dude"},{"value":"mary","comment":"nice lady"},{"value":"jimmy","comment":"very uncool guy"},{"value":"jimbo","comment":null}]' />

The component uses the autocompletesearchparam attribute or searchParam property to allow the developer to define a set of data to use for autocompletion.

Note the format of the JSON. The comment property is optional and may be omitted from the JSON. Be sure to include it if you use the showcommentcolumn="true" textbox attribute.

In case you do not like the standard tree layout of the autocomplete popup, you can also trigger the use of a richlistbox by linking a panel with the type attribute set to "autocomplete-richlistbox" like this:

<panel id="richPopup" type="autocomplete-richlistbox" />

<textbox id="text1" type="autocomplete" autocompletesearch="simple-autocomplete"
         showcommentcolumn="true" autocompletepopup="richPopup"
         autocompletesearchparam='[{"value":"mark","comment":"cool dude"},{"value":"mary","comment":"nice lady"},{"value":"jimmy","comment":"very uncool guy"},{"value":"jimbo","comment":null}]' />

How to solve autocomplete panel focus problems

In some situations there may be problems with the display of the autocomplete panel (this is the panel where the autocomplete results/suggestions are displayed): the autocomplete panel seems to appear seldom and very erratically. This can happen when using an autcomplete panel in a panel (for example if an autcomplete textbox is set in a panel that shows and hides). The solution is to define a custom autocomplete panel as follows with the noautofocus="true" attribute (very important, don't miss this part):

<panel id="popup_autocomplete" type="autocomplete" noautofocus="true" />

<textbox type="autocomplete"
         autocompletesearch="custom-autocomplete"
         autocompletepopup="popup_autocomplete" />

There is another example of the use of an autocomplete panel in Autocomplete code snippets.

Tips

Clear caches!
If you are using xulrunner please don't forget that changes will only take place when you either disable caching or run xulrunner with -purgecaches

See also