Friday, August 3, 2007

Mastering ADF Faces <af:selectOneChoice> component

I've being trying to solve this issue for a long time. I've read many blogs, forums and articles about it but, none was a complete solution. Fortunately, now that I've finally managed to put it all together I decided to share with you all.

The problem is very simple and, as far as I noticed, very common also. I want to build a combo box based on a result set from my database within an ADF Faces page. Ok, I know, I should use the selectOneChoice component bound to my database through a PageDefinition XML. What if I also need to bind a specific attribute from my result set to the value attribute of each item in the selectOneChoice component? Well, believe me... it's not as easy as it may seem.

By definition, when a bound selectItems tag is used with the seleceOneChoice component, ADF will render each item like this:

<option value="N">bound resultset attribute as the option label</option>

"N" being a natural number varying from 0 (zero) to the total number of rows in the result set minus 1. In other words, an item's value will always be the index of it's corresponding row in the bound iterator's collection, no matter what. The problem is that, many times, it's really important to have more meaningful information as the item's value, specially when we need to make some client-side processing based on it. The question is: how? Well, here is the complete answer with very simple example.

Suppose we've defined a list binding identified by "myList" bound to an iterator for a collection of objects of the following bean class:

public final class ListItemBean {

   private String itemValue;
   private String itemLabel;

   public ListItemBean() {}

   public void setItemValue(String value) {
      this.itemValue = value;
   }

   public void setItemLabel(String label) {
      this.itemLabel = label;
   }

   public String getItemValue() {
      return this.itemValue;
   }

   public String getItemLabel() {
      return this.itemLabel;
   }
}


The most common use case of <af:seleconechoice> would be:

<af:selectonechoice id="myCombo" value="#{bindings.myList.inputValue}">
   <f:selectitems value="#{bindings.myList.items}"/>
</f:selectitems>


The label of each will be rendered according to myList definition in the PageDefinition XML file. So, if I want to use the bean attribute itemLabel, myList definition should be something like this:

<list listopermode="1" iterbinding="myListIterator" id="myList">
   <attrnames>
      <item value="itemLabel">
   </item>
</attrnames>


Now, if I want to take control of how each will be rendered:

<af:selectonechoice id="myCombo" valuepassthru="true" value="#{bindings.myList.inputValue}">
   <af:foreach items="#{bindings.myList.iteratorBinding.allRowsInRange}" var="row">
      <af:selectItem id="myItem"
         value="#{row.dataProvider.itemValue}"
         label="#{row.dataProvider.itemLabel}"/>
   </af:forEach>
</af:selectOneChoice>


It is very important to define an id for af:selectitem. If you don't, ADF runtime won't render the page correctly. Also notice the valuePassThru attribute defined to "true". It tells ADF to render each selectItem like this: <option value="the item's real value goes here">the item's label goes he</option>. That's important exactly because I need to make client-side processing using items real values. Without valuePassThru="true", the options values would continue to be rendered as corresponding indexes. Before you ask, know that simply adding valuePassThru="true" to the common use case above won't work since ADF ignores it if you aren't using af:selectItem.

Well, that should be all, but it isn't. There's one side effect which is a potentially unwanted empty element as your combo box's first option. That's because now, ADF doesn't have a default value for your combo box when it's first rendered. There are many different ways of solving this minor issue but, in my opinion, the easiest would be to add the following JavaScript to the page:

<script type="text/javascript">

function removeEmptyOption() {
   if (document.forms[0].myCombo.options[0].value=='') {
      document.forms[0].myCombo.options[0] = null;
      document.forms[0].myCombo.value = document.forms[0].myCombo.options[0].value;
   }
}

</script>


Now, just call it from the page body's onLoad event: <afh:body onload="removeEmptyOption()">

If you prefer, you may also use CSS to do the job (which I think is far more elegant). Just add the following style to the page:

<style type="text/css" media="screen">
   option[value=""] {
      display: none;
   }
</style>


The problem with this solution is that CSS Selectors are not compatible with all browsers, specially with IE6 : (

So, that's it. I hope you enjoy.