AutoComplete for JSF SelectOneMenu component

On the net you can navigate over the options of a comboBox according to the first letter of the content. If you handle the onKeyPress event of comboBox then you can support navigation for all of the characters of options in the comboBox. Now user can navigate through the options by pressing characters on the focused comboBox. Here is the way to do it.

You need two global variables to hold the time out and pressed keys.

var combo_text = "";
var combo_timeout_id = null;

The onKeyPress function.

function onComboBoxKeyPress(event)
      var e = event.srcElement;

      if (combo_timeout_id)
      combo_text += String.fromCharCode(event.keyCode);
      var index = find_closest_item_index(e, combo_text);
      event.returnValue = false;
      e.selectedIndex = index;
      combo_timeout_id = window.setTimeout("special_combo_timed_out()", 500);

This function returns the best matching result by comparing the user-typed characters to comboBox options' value.

function find_closest_item_index(e, combo_text)
      var opt = e.options;
      var max_match_index = 0;
      var max_match = 0;

      for (i = 0; i < opt.length; i++)
            var match_count = matching_char_count(opt(i).text, combo_text);

            if (match_count > max_match)
                  max_match = match_count;
                  max_match_index = i;
      return max_match_index;

To find the number of matching characters...

function matching_char_count(text, pattern)
      var i = 0;
      var len = pattern.length;

      if (text.length < pattern.length)
            len = text.length;
      while (i < len)
            if (text.charAt(i) != pattern.charAt(i))
      return i;

Time out value for the user tpyings (0.5 sec).

function special_combo_timed_out()
      combo_text = "";

To enable this property for all the JSF SelectOneMenu you should define the onKeyPress event for all comboBoxes. So you have to define your own MenuRenderer in the renderkit of your faces-config.xml.
Add the new renderer to the renderkit in the faces-config.xml


The MenuRenderer will be slightly different from com.sun.faces.renderkit.html_basic.MenuRenderer with the renderingScript method and onKeyPress changes. 

You should add onKeyPress script to HtmlSelectOneMenu in the rendering method.

if (((HtmlSelectOneMenu)uicomponent).getOnkeypress() == null)
            responsewriter.writeAttribute("onkeypress", (?onComboBoxKeyPress(event);?, null);
  (((HtmlSelectOneMenu)uicomponent).getOnkeypress() != null)
            responsewriter.writeAttribute("onkeypress", ((HtmlSelectOneMenu)uicomponent).getOnkeypress(), null);

Now the last thing to do is to render the script that includes all the scripting stuff that i gave above. The script include tag should only be rendered for once per page so we should define two global variables in the MenuRenderer class to handle this situation

 private static final String COMBO_SORT_SCRIPT_RENDERED = "comboautocomplete-script-rendered";

 private static final String COMBO_SORT_SCRIPT_VIEW_ID = "comboAutoComplete.js";

COMBO_SORT_SCRIPT_VIEW_ID specifies should be same as the .js file. Here is the renderScriptOnce method. You should invoke it at the encodeBegin method of the renderer.

private void renderScriptOnce(ResponseWriter writer, UIComponent component, FacesContext context) throws IOException
{      Map requestMap = context.getExternalContext().getRequestMap();
      Boolean scriptRendered = (Boolean) requestMap.get(COMBO_SORT_SCRIPT_RENDERED);
      if (scriptRendered == Boolean.TRUE) {
      requestMap.put(COMBO_SORT_SCRIPT_RENDERED, Boolean.TRUE);

      writer.startElement("script", component);
      writer.writeAttribute("type", "text/javascript", null);
      String src = "scripts/" + COMBO_SORT_SCRIPT_VIEW_ID;
      writer.writeAttribute("src", src, null);