Enhanced rich:comboBox: Values (not only Text) and RegExp Suggestions

Ever wondered how to work with values (not only text) when selecting options suggested by rich:comboBox component?

What about a better search method for client-side suggestions?

I needed to do this, so I came up with a solution...
Hope it helps you!

Motivation

  • rich:comboBox does not support custom values for items in the suggestion list, because, as stated in the examples, "When using selectItem(s) for suggestion definition you should define only value of the selectItem. It is used both for displaying and storing", so the itemValue is used as if it was the itemLabel and the real itemValue cannot be used.
  • Sometimes the selected text is not really needed, but the underlying value associated to it, maybe an id to an object in the DB...
  • The suggestions could be more interesting than based on a simple "begins with" search.

 

Overview

As I don't know how to pass the following behaviour to the Java component itself, this JavaScript code redefines some specific functions of Richfaces.ComboBoxList class, in order to:
  • Support an optional array of custom values "itemsValue[]", associated by index to the existing "itemsText[]" array.
  • Store current selected custom value in an inner "selectedItemValue" variable.
  • Support suggestions based on a regular expression which matches every typed string, separated by spaces, as a substring of the available texts. For example, if there is a text "New York City", "N Y C" should match.
  • Convert accentued non ASCII characters (with utility function), so it would be the same to search for "Sao Paulo" and "São Paulo".
  • Support an optional array of excluded custom values "itemsValueExclude[]", so the corresponding texts will not be listed in the suggestions.

 

Directions

  • Load this JS code at the beginning of your page or template (inline or as a resource), so it to actually overrides Richfaces.ComboBoxList (check with Firebug the members of Richfaces.ComboBoxList.prototype). You can find the RichFaces original code in: richfaces-ui-3.3.3.Final.jar/org/richfaces/renderkit/html/scripts/combolist.js. At the end of this article you can see the enhanced combolist.js (file is also attached).

 

<a:loadScript src="/js/combolist.js" />

 

  • Define an input hidden tag that will actually store the custom value. It may not be associated to any backing bean property. It is needed because the stored custom value "selectedItemValue" will be updated every time the comboBox finds a match, and not only when the user selects a valid option, so it's not totally reliable.

 

<h:inputHidden id="myValue" />

 

  • Define a JS function that should clear the input hidden value when no valid text is selected. I recommend calling it on page load.
  • Also define a JS function that will set the input hidden value when a valid text is selected.

 

<script type="text/javascript">
//<![CDATA[
function myValueClearFn() {
  jQuery('#myValue').val(null);
}
function myValueUpdaterFn() {
  jQuery('#myValue').val(#{rich:component('myComboBox')}.comboList.selectedItemValue);
}
//]]>
</script>

  • Define the rich:comboBox.
    • As you don't really need the selected text, don't define the value attribute.
    • Don't define the suggestionsList, we'll define it manually.
    • It's good to selectFirstOnUpdate.
    • The onlistcall attribute should call myValueClearFn().
    • The onselect attribute should call myValueUpdaterFn().

 

<rich:comboBox id="myComboBox"
      width="350" listWidth="400" listHeight="200"
      selectFirstOnUpdate="true"
      onlistcall="myValueClearFn()"
      onselect="myValueUpdateFn()" />

 

  • Manually define labels and values (itemsText[] and itemsValue[] arrays), where elements should be ordered so they match each other based on their index.
  • Array itemsValue[] should contain primitive data types only (integer or string).
  • Also define the itemsValueExclude[] array, where items should be of the same data type of elements in itemsValue[] (for the array indexOf search to work fine).
  • You could define all these arrays just after rich:comboBox creation or later, based on some condition of your app.

 

<script type="text/javascript">
//<![CDATA[
#{rich:component('myComboBox')}.comboList.itemsText  = ['Java Programming Language', 'PHP Hypertext Preprocessor', 'Active Server Pages', ...];
#{rich:component('myComboBox')}.comboList.itemsValue = ['java', 'php', 'asp', ...];
#{rich:component('myComboBox')}.comboList.itemsValueExclude = ['asp', ...];
//]]>
</script>

  • You could also get some of these arrays from a backing bean, which should return them as a string in JSON JSArray format.

 

<script type="text/javascript">
//<![CDATA[
#{rich:component('myComboBox')}.comboList.itemsText  = #{myBean.labelsAsJSArray};
#{rich:component('myComboBox')}.comboList.itemsValue = #{myBean.valuesAsJSArray};
//]]>
</script>

  • The selected custom value stored in the input hidden will be available via jQuery('#myValue').val(). You could send it to your backing bean for processing as an a4j:actionparam passed to some AJAX enabled action. I like aj4:jsFunction, for example:

 

 

<a4j:jsFunction id="myAjaxFn" name="myAjaxFn" action="#{myBean.someAction}">
  <a4j:actionparam name="value"
      value="jQuery('#myValue').val()"
      assignTo="#{myBean.theValue}"
      noEscape="true" />
</a4j:jsFunction

 

 

Enhanced combolist.js

This is the needed JS code to enhance rich:comboBox. You may also download the attachment.
Richfaces.ComboBoxList.addMethods({

  createDefaultList : function() {
    var items = new Array();
    for (var i = 0; i < this.itemsText.length; i++) {
      // if itemsValue[] is defined, send also the custom value to this.createItem()
      if (this.itemsValue)
        items.push(this.createItem(this.itemsText[i], this.classes.item.normal, this.itemsValue[i]));
      else
        items.push(this.createItem(this.itemsText[i], this.classes.item.normal));
    }
    this.createNewList(items);
  },

  doSelectItem : function(item) {
    this.selectedItem = item;
    // Save selected item's value locally, which should be stored in the 'id' attribute of the 'span' tag
    if (item != null && item.id != null)
      this.selectedItemValue = item.id;
  },

  getFilteredItems : function(text) {
    var items = new Array();
    // Filter converted to ASCII, lower-cased, escaped, trimmed, parentheses escaped for the RegExp
    var filter = convertNonAscii(text.toLowerCase()).escapeHTML().replace(/^\s\s*|\s\s*$/, "").replace(/\(/, "\\(").replace(/\)/, "\\)");
    // RegExp that applies AND operation over all written words, separated by spaces
    var regexp = new RegExp("^(?=.*" + filter.replace(/\s\s/g, " ").replace(/\s/g, ")(?=.*") + ").*", "gi");
    for (var i = 0; i < this.itemsText.length; i++) {
      //var itText = this.itemsText[i];
      var itText = convertNonAscii(this.itemsText[i].toLowerCase());
      // Search should be based on the RegExp and not on "contains" nor "begins with"
//      if (itText.substr(0, text.length).toLowerCase() == text.toLowerCase()) { //FIXME: to optimaize
//      if (itText.indexOf(filter) != -1) {
      if (itText.match(regexp)) {
        // custom values defined ?
        if (this.itemsValue) {
          // exclude item if there is no itemsValueExclude[] array or if custom value is not present in it
          if (this.itemsValueExclude == null || this.itemsValueExclude.indexOf(this.itemsValue[i]) == -1)
            items.push(this.createItem(this.itemsText[i], this.classes.item.normal, this.itemsValue[i]));
        } else
          items.push(this.createItem(this.itemsText[i], this.classes.item.normal));
      }
    }
    return items;
  },

  findItemBySubstr : function(substr) {
    var items = this.getItems();
    // Filter converted to ASCII, lower-cased, escaped, trimmed, parentheses escaped for the RegExp
    var filter = convertNonAscii(substr.toLowerCase()).escapeHTML().replace(/^\s\s*|\s\s*$/, "").replace(/\(/, "\\(").replace(/\)/, "\\)");
    // RegExp that applies AND operation over all written words, separated by spaces
    var regexp = new RegExp("^(?=.*" + filter.replace(/\s\s/g, " ").replace(/\s/g, ")(?=.*") + ").*", "gi");
    for (var i = 0; i < items.length; i++) {
      var item = items[i];
      //var itText = item.innerHTML.unescapeHTML();
      var itText = convertNonAscii(item.innerHTML.unescapeHTML().toLowerCase());
      // Search should be based on the RegExp and not on "contains" nor "begins with"
//      if (itText.substr(0, substr.length).toLowerCase() == substr.toLowerCase()) { //FIXME: to optimaize
//      if (itText.indexOf(filter) != -1) {
      if (itText.match(regexp)) {
//        return item;
        // custom values defined ?
        if (this.itemsValue) {
          // exclude item if there is no itemsValueExclude[] array or if custom value is not present in it
          if (this.itemsValueExclude == null || this.itemsValueExclude.indexOf(this.itemsValue[i]) == -1)
            return item;
        } else
          return item;
      }
    }
  },

  createItem : function(text, className, value) {
    // receive the custom value
    var escapedText = text.escapeHTML();
    // Save custom value as 'id' in 'span', so it can be recovered later
    if (value != null)
      return "<span id=\"" + value + "\" class=\"" + className+ "\">" + escapedText + "</span>";
    else
      return "<span class=\"" + className+ "\">" + escapedText + "</span>";
  }

});

 

String manipulation utils.js

Special thanks to Real Gagnon. You may also download the attachment.
/**
 * String manuipulation Utils
 * Code ported from Real's Java HowTo: http://www.rgagnon.com/javadetails/java-0456.html
 * Author: Luis Tama Wong
 */

var PLAIN_ASCII =
    "AaEeIiOoUu" // grave
  + "AaEeIiOoUuYy" // acute
  + "AaEeIiOoUuYy" // circumflex
  + "AaOoNn" // tilde
  + "AaEeIiOoUuYy" // umlaut
  + "Aa" // ring
  + "Cc" // cedilla
  + "OoUu" // double acute
;

var UNICODE =
    "\u00C0\u00E0\u00C8\u00E8\u00CC\u00EC\u00D2\u00F2\u00D9\u00F9"
  + "\u00C1\u00E1\u00C9\u00E9\u00CD\u00ED\u00D3\u00F3\u00DA\u00FA\u00DD\u00FD"
  + "\u00C2\u00E2\u00CA\u00EA\u00CE\u00EE\u00D4\u00F4\u00DB\u00FB\u0176\u0177"
  + "\u00C3\u00E3\u00D5\u00F5\u00D1\u00F1"
  + "\u00C4\u00E4\u00CB\u00EB\u00CF\u00EF\u00D6\u00F6\u00DC\u00FC\u0178\u00FF"
  + "\u00C5\u00E5"
  + "\u00C7\u00E7"
  + "\u0150\u0151\u0170\u0171"
;

// remove accentued from a string and replace with ascii equivalent
function convertNonAscii(s) {
  if (s == null)
    return null;
  var sb = '';
  var n = s.length;
  for (var i = 0; i < n; i++) {
    var c = s.charAt(i);
    var pos = UNICODE.indexOf(c);
    if (pos > -1) {
      sb += PLAIN_ASCII.charAt(pos);
    } else {
      sb += c;
    }
  }
  return sb;
};

 

Finally...

This an other things can be done when looking at the JS code behind RichFaces. Hope I can share more tricks like this one.

So, please test this code and tell me what you think... Any comments really appreciated...

 

God bless you all.

 

Best regards,

Luis Tama Wong