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.

10 comments:

Anonymous said...

Can u please post the example of selectonechoice with valueChangeListener attribute?

Hiren

Dave Jarvis said...

Slightly more maintainable version:

function removeEmptyOption() {
var combo = document.forms[0].myCombo;

if( combo.options[0].value == '' ) {
combo.options[0] = null;
combo.value = combo.options[0].value;
}
}

Sourav said...

Hi,
Its a very nice article ,but to make it complete if you would have provided on how to acess the value of the drop down or combo from your backing bean then it would have been great ,as all the problem am facing is there.I tried to get the value from the dropdown which retuns me index ,when i tried to get it from selecList it gives me value but as i attach binding method to af:SelectItem it doesn't show multiple entries in dropdown i.ee it shows one entry only.Please through some light in it.
Thanks

Anonymous said...

I don't understand why you choose listOperMode=1 instead of using listOperMode=0 ...

Sriram said...

Thank you for sharing this information. Helps all who are new to ADF.

Regards,
Sri

Anonymous said...

When dealing with this issue I also found the following link complementary.

http://ranajitsahoo.blogspot.com/2008/05/how-to-implement-dependent-drop-down.html

Anonymous said...

Your going to run into some problems pulling the "mycombo" component off the dom because it's id will change. I would suggest pulling all the select components with getElementByTagName, then searching the id's of those components using .indexOf("mycombo")

Arju said...

af:selectItem id="myItem"
value="#{row.dataProvider.itemValue}"
label="#{row.dataProvider.itemLabel}"/

Got the above code....

But
What is the dataProvider?? Where have to define that??

Arju said...

af:selectItem id="myItem"
value="#{row.dataProvider.itemValue}"
label="#{row.dataProvider.itemLabel}"/

Not getting idea,What is DataProvider?? Where have to define that??

Eduardo Ribeiro Rodrigues said...

Arju,

row.dataProvider is an implicit object defined by the underlying ADF layers.