In keeping with a short tradition, today is another day during my Christmas break on which I had some time to dabble in Java Server Faces and particularly the creation of custom JSF components. This article shows a new component that may actually make sense in real applications. Let me give you a little background: most applications present the end user with text fields where the user can type in new values. Most applications also have selection components through which the user can select one of a set of allowable values. These selection components can have various shapes, such as list box, dropdownlist and radio buttons. Depending on several considerations, such as the look and feel requirements of the interaction designers and end users and the number of options to be presented in the selection component, a specific component is used in a page.
Common wisdom has it that for a small number of allowable values, radio buttons – laid out horizontally or vertically – are a good choice. With an increasing number of options, a drop downlist or list-box may be better, as they occupy less real estate than the full set of radio buttons would. The values to choose from are still very much accessible to the end user. For a really large number of options, another component usually comes into play: the List of Values, either in a floating DIV, a popup window or an AJAX powered suggestion element.
There is a challenge however with this common wisdom, apart from the question where exactly lies the boundary between a small and larger number of allowable values. And that is: we may not know beforehand, at design time, what the number of allowable values is going to be. If these values are retrieved from a dynamic collection, perhaps fueled by a web service or database query, the size of the set of value can vary wildly. So any choice made during development for either radio buttons or drop downlist may be invalid at run time. So: do we throw the common wisdom out of the window?
Well, this is exactly where the custom component presented in this article comes in. The SelectSwitcher is a component that wraps multiple select-components in its facets. Depending on the number of select-options – determined dynamically at run time – one of the facets is rendered. So depending on first of all the number of options available to the user and second of all the boundary between a small and large number of options, the page will display either the one or the other selection component.
Illustrated in a simple screen shot: we have a selection element five colors to choose from. Our SelectSwitcher contains three facets: one for a small set of options (verticallay laid out radio buttons) , one for a large set (dropdown list) and one for the read only situation (where no options need to be presented at all). With the first radio group (labeled Set the number of selections where small becomes large) we can specify when we switch from a small set of selection options to a large set.
With the same set of five colors to choose from, when we change this value from 6 to 4, we essentially tell the application that our set of colors is now a large set, and therefore should be displayed as dropdown list:
Of course we would have the same effect if the set of colors had dynamically changed from a small one to a large one.
The article goes into a few relatively interesting aspects of custom JSF development, including the use of facets, accessing child components during rendering, manipulating children and transfering both child-elements and attributes etc. When I started out, it seemed a real challenge. Now the component is developed as working prototype, the size of the required code base is breathtakingly small. That must be the power and joy of JSF I suppose.
The JSF snippet
for using the SelectSwitcher element to achieve the effect demonstrated in the screenshots is the following:
<AMISJSF:SelectSwitcher id="colorSelectSwitcher"
readonly="#{DemoSelectSwitcherBean.readOnly}"
switchIndex="#{DemoSelectSwitcherBean.switchValue}"
value="#{DemoSelectSwitcherBean.selectValue}">
<f:facet name="small">
<h:panelGrid columns="2">
<af:outputText value="Choose a color from this small selection"/>
<af:selectOneRadio layout="vertical"/>
</h:panelGrid>
</f:facet>
<f:facet name="large">
<h:panelGrid columns="2">
<af:outputText value="Choose a color from this long list"/>
<af:selectOneChoice />
</h:panelGrid>
</f:facet>
<f:facet name="readonly">
<h:panelGrid columns="2">
<af:outputText value="Here is the read only value for the selected color"/>
<af:outputText value="READONLY" id="here"/>
</h:panelGrid>
</f:facet>
<f:selectItem itemLabel="Blue" itemValue="XXX"/>
<f:selectItem itemLabel="Yellow" itemValue="YYY"/>
<f:selectItem itemLabel="Green" itemValue="GGG"/>
<f:selectItem itemLabel="Red" itemValue="RRR"/>
<f:selectItem itemLabel="White" itemValue="WWW"/>
</AMISJSF:SelectSwitcher>
Let’s take a look at the implementation. We have the same combination of artefacts as before: an HtmlSelectSwitcher UIComponent Class, a corresponding Tag Class, a TLD entry (tag library description) and a registration in the faces-config.xml file.
First we will have the trivial stuff out of the way.
1. The UIComponentTag class
package nl.amis.jsf.tags;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import javax.faces.webapp.UIComponentTag;
import nl.amis.jsf.HtmlSelectSwitcher;
public class SelectSwitcherTag extends UIComponentTag {
String switchIndex;
String value;
String disabled;
String readonly;
public SelectSwitcherTag() {
}
protected void setProperties(UIComponent component) {
super.setProperties(component);
if (switchIndex != null) {
if (isValueReference(switchIndex)) {
FacesContext context = FacesContext.getCurrentInstance();
Application app = context.getApplication();
ValueBinding vb = app.createValueBinding(switchIndex);
component.setValueBinding(HtmlSelectSwitcher.ATTRIBUTE_SWITCHINDEX,
vb);
} else {
component.getAttributes().put(HtmlSelectSwitcher.ATTRIBUTE_SWITCHINDEX,
switchIndex);
}
}
if (value != null) {
if (isValueReference(value)) {
FacesContext context = FacesContext.getCurrentInstance();
Application app = context.getApplication();
ValueBinding vb = app.createValueBinding(value);
component.setValueBinding("value", vb);
} else {
component.getAttributes().put("value", switchIndex);
}
}
if (readonly != null) {
if (isValueReference(readonly)) {
FacesContext context = FacesContext.getCurrentInstance();
Application app = context.getApplication();
ValueBinding vb = app.createValueBinding(readonly);
component.setValueBinding(HtmlSelectSwitcher.ATTRIBUTE_READONLY,
vb);
} else {
component.getAttributes().put(HtmlSelectSwitcher.ATTRIBUTE_READONLY,
readonly);
}
}
if (disabled != null) {
if (isValueReference(disabled)) {
FacesContext context = FacesContext.getCurrentInstance();
Application app = context.getApplication();
ValueBinding vb = app.createValueBinding(disabled);
component.setValueBinding("disabled", vb);
} else {
component.getAttributes().put("disabled", disabled);
}
}
}
public void release() {
super.release();
}
public String getComponentType() {
return "SelectSwitcher";
}
public String getRendererType() {
return null;
}
public void setSwitchIndex(String switchIndex) {
this.switchIndex = switchIndex;
}
public String getSwitchIndex() {
return switchIndex;
}
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setDisabled(String disabled) {
this.disabled = disabled;
}
public String getDisabled() {
return disabled;
}
public void setReadonly(String readonly) {
this.readonly = readonly;
}
public String getReadonly() {
return readonly;
}
}
This class intermediates between the selectSwicther tag for this JSF component in the JSP page and the UIComponent class. It handles a number of attributes we can define for the tag in the JSP page and makes them available to the UIComponent class, dealing with straight values as well as ValueBindings.
2. Describe the Tag in the Tag Library Description
The TLD file has the following entry:
<tag>
<description>Switch between Radiobuttons and Dropdownlist depending on number of select items</description>
<display-name>Switch Select Component</display-name>
<name>SelectSwitcher</name>
<tag-class>nl.amis.jsf.tags.SelectSwitcherTag</tag-class>
<body-content>JSP</body-content>
<attribute>
<name>id</name>
</attribute>
<attribute>
<name>rendered</name>
</attribute>
<attribute>
<name>readonly</name>
</attribute>
<attribute>
<name>disabled</name>
</attribute>
<attribute>
<name>binding</name>
</attribute>
<attribute>
<name>value</name>
</attribute>
<attribute>
<name>switchIndex</name>
</attribute>
<example>Brief snippet showing how to use this tag.</example>
</tag>
3. Register the JSF Component in the faces-config.xml file
All JSF components need to be registered in a faces-config.xml file. Either the main one in WEB-INF, or one in a META-INF subdirectory of one of the JAR files in WEB-INF\lib. Or in one of the files listed in the context parameter javax.faces.CONFIG_FILES
in the web.xml file. The component registration for SelectSwitcher looks like this:
<component>
<component-type>SelectSwitcher</component-type>
<component-class>nl.amis.jsf.HtmlSelectSwitcher</component-class>
</component>
The important stuff: the HtmlSelectSwitcher UIComponent class
Now I will describe my intention with the SelectSwitcher: I want to be able to insert two SelectOne… components in my JSP page – one for small sets of values and one for large sets. I want to be able to easily define, both with a hard-coded value as well as with a valuebinding, what the number of allowable values is that specifies the boundary between a small set and a large set.
I only want to define the SelectItem and SelectItems elements – with the allowable values themselves – once. Other attributes such as readOnly, rendered and JavaScript eventhandlers should also be defined only once. Ideally I should be able to define a special case for the situation of readOnly==true. In that situation, I do not want to display a SelectOne…, but instead perhaps an OutputText that displays the Label (not the value!) that is currently set.
It would be nice if the small-set-of-values and large-set-of-values situations would allow me to do a little bit more than only specify a SelectOne.. element. It would be even better to be able to define a container element, labels etc. For example by using a facet for each situation…
Well, we have already seen the final result that could be used in this way:
<AMISJSF:SelectSwitcher id="colorSelectSwitcher"
readonly="#{DemoSelectSwitcherBean.readOnly}"
switchIndex="#{DemoSelectSwitcherBean.switchValue}"
value="#{DemoSelectSwitcherBean.selectValue}">
<f:facet name="small">
<h:panelGrid columns="2">
<af:outputText value="Choose a color from this small selection"/>
<af:selectOneRadio layout="vertical"/>
</h:panelGrid>
</f:facet>
<f:facet name="large">
<h:panelGrid columns="2">
<af:outputText value="Choose a color from this long list"/>
<af:selectOneChoice />
</h:panelGrid>
</f:facet>
<f:facet name="readonly">
<h:panelGrid columns="2">
<af:outputText value="Here is the read only value for the selected color"/>
<af:outputText value="READONLY" id="here"/>
</h:panelGrid>
</f:facet>
<f:selectItem itemLabel="Blue" itemValue="XXX"/>
<f:selectItem itemLabel="Yellow" itemValue="YYY"/>
<f:selectItem itemLabel="Green" itemValue="GGG"/>
<f:selectItem itemLabel="Red" itemValue="RRR"/>
<f:selectItem itemLabel="White" itemValue="WWW"/>
</AMISJSF:SelectSwitcher>
Here we see the three facets for the cases of readOnly, small set and large set. Each facet can contain one component, which can be a container such as panelGrid that in turn can contain many elements.
The allowable values are defined just once, as child elements of SelectSwitcher. They will be assigned to the active SelectOne component at run time. Note that even though in this snippet we have a set of static selectItem elements, they might just as well be dynamic – bound – selectItems elements.
The SelectSwitcher element has a value attribute, that has been specified with a ValueBinding, an EL expression that binds it to the selectValue property in the DemoSelectSwitcherBean. At run time, this value binding will be transfered to the active SelectOne.. component.
The switchIndex attribute is the one we can use to specify the boundary between a small set of allowable values and a big one. If the actual set of values has more elements than switchIndex, the SelectSwitcher will render the large facet. If the number of elements is less than or equal to switchIndex, the small facet is rendered. Note that if readOnly evaluates to true, neither small nor large is rendered but the readOnly facet instead, if it exists. In that case, the SwitcherElement will look for a UIComponent inside the readOnly facet with id equals here. It will set the Label for the current value to the value attribute of this UIComponent.
The code for the HtmlSelectSwicther class:
package nl.amis.jsf;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIComponentBase;
import javax.faces.component.UIInput;
import javax.faces.component.UISelectItem;
import javax.faces.component.UISelectItems;
import javax.faces.component.ValueHolder;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import oracle.adfinternal.view.faces.renderkit.uix.SelectItemSupport;
public class HtmlSelectSwitcher extends UIComponentBase implements ValueHolder {
public HtmlSelectSwitcher() {
}
public static final String FACET_SMALL = "small";
public static final String FACET_LARGE = "large";
public static final String FACET_READONLY = "readonly";
public static final String ATTRIBUTE_READONLY = "readonly";
public static final String ATTRIBUTE_SWITCHINDEX = "switchIndex";
public void encodeEnd(FacesContext context) throws IOException {
}
public void encodeChildren(FacesContext FacesContext) throws IOException {
super.encodeChildren(FacesContext);
}
public void encodeBegin(FacesContext FacesContext) throws IOException {
super.encodeBegin(FacesContext);
String switchIndexString = (String)getAttributes().get(ATTRIBUTE_SWITCHINDEX);
Integer switchIndex = new Integer(switchIndexString);
if ((switchIndex == null) || (switchIndex.intValue() == 0)) {
switchIndex = new Integer(5);
}
List kids = getChildren();
int childCount = this.getChildCount();
List siList = new ArrayList();
int sicount = 0;
for (int i = 0; i < childCount; i++) {
if (kids.get(i) instanceof UISelectItem) {
siList.add((UISelectItem)kids.get(i));
} else if (kids.get(i) instanceof UISelectItems) {
siList.add((UISelectItems)kids.get(i));
}
}
// this next method calls is only meaningful if this implements ValueHolder
sicount = SelectItemSupport.getSelectItemCount(this);
Boolean readonly = Boolean.FALSE;
if (getAttributes().containsKey(ATTRIBUTE_READONLY)) {
Object ro = getAttributes().get(ATTRIBUTE_READONLY);
if (ro instanceof String) {
readonly = new Boolean((String)ro);
} else if (ro instanceof Boolean) {
readonly = (Boolean)ro;
}
} else if (getValueBinding(ATTRIBUTE_READONLY) != null) {
readonly =
(Boolean)getValueBinding(ATTRIBUTE_READONLY).getValue(FacesContext);
}
UIComponent select = null;
UIComponent facet = null;
Object facets = this.getFacets();
facet = findFacet(FACET_LARGE, this);
select = findFirstInput(facet);
// if rendered is not false, the input component would still try do decode itself
// and would reset the value in doing so
select.setRendered(false);
facet = findFacet(FACET_SMALL, this);
select = findFirstInput(facet);
select.setRendered(false);
if (readonly) {
facet = findFacet(FACET_READONLY, this);
UIComponent target = facet.findComponent("here");
facet.setRendered(true);
facet.getAttributes().put("title",
"This is the panelgrid for the readonly situation");
Object value = getAttributes().get("value");
if (getValueBinding("value") != null) {
value = getValueBinding("value").getValue(FacesContext);
}
for (UISelectItem si: (List<UISelectItem>)siList) {
if ((si.getItemValue()).equals(value)) {
target.getAttributes().put("value", si.getItemLabel());
break;
}
}
} else {
if (sicount >
switchIndex.intValue()) { // larger than cut-off value
facet = findFacet(FACET_LARGE, this);
select = findFirstInput(facet);
} else {
facet = findFacet(FACET_SMALL, this);
select = findFirstInput(facet);
}
select.setRendered(true);
copyAttribute(ATTRIBUTE_READONLY,"readOnly", this, select);
copyAttribute("value","value", this, select);
int num = select.getChildCount();
for (int i = 0; i < num; i++) {
select.getChildren().remove(0);
}
select.getChildren().addAll(siList);
}
// now make sure that the facet gets rendered, along with its children
facet.encodeBegin(FacesContext);
facet.encodeChildren(FacesContext);
facet.encodeEnd(FacesContext);
}
private UIComponent findFacet(String facetName, UIComponent component) {
// note: getFacets.get() returns the (only) UIComponent within the Facet!!
return (UIComponent)component.getFac ets().get(facetName);
}
private UIComponent findFirstInput(UIComponent component) {
if (component == null)
return null;
if ((component instanceof EditableValueHolder)||(component instanceof UIInput))
return component;
for (UIComponent child: (List<UIComponent>)component.getChildren()) {
if ((child instanceof EditableValueHolder) ||(child instanceof UIInput))
return child;
else {
UIComponent input = findFirstInput(child);
if (input != null)
return input;
}
}
return null;
}
private void copyAttribute(String sourceAttributeName, String targetAttributeName, UIComponent sourceComponent, UIComponent targetComponent) {
if (sourceComponent.getAttributes().containsKey(sourceAttributeName)) {
targetComponent.getAttributes().put(targetAttributeName, sourceComponent.getAttributes().get(sourceAttributeName));
} else if (sourceComponent.getValueBinding(sourceAttributeName) != null) {
targetComponent.setValueBinding(targetAttributeName,
sourceComponent.getValueBinding(sourceAttributeName));
}
}
public Object getLocalValue() {
return null;
}
public Object getValue() {
// get value from rendered child
return null;
}
public void setValue(Object object) {
// pass value to rendered child??
}
public void decode(FacesContext facesContext) {
super.decode(facesContext);
}
public Converter getConverter() {
return null;
}
public void setConverter(Converter converter) {
}
public String getFamily() {
return null;
}
}
Going over the code – and it is by no means polished and perfect – the important things are:
1. Determine the number of SelectItem children in the SelectSwitcher element by calling the SelectItemSupport.getSelectItemCount() method
2. Build a Collection with SelectItem children – to transfer eventually to the SelectOne.. element on duty
3. Check whether the SelectSwitcher element is set to readonly
4. Set the first Input Element in both Large and Small facet to non-rendered (we assume the first Input element is in fact the SelectOne… in those facets); we set them to non-rendered to be sure that later on the SelectOne.. element that is not on duty is not accidentally seen as rendered as therefore will decode its value and update its ValueBinding, resetting it since the SelectOne.. element not on duty will not find its value in the request.
5. If the SelectSwitcher is readonly, locate the readonly facet; locate the element with ID equals here and set the value of that element equal to the label for the current value of the SelectSwitcher; the label is determined by looking for the value among the SelectItem elements and taking the ItemLabel for the SelectItem whose ItemValue matches.
6. If not read only, then if we are looking at a large set of values, find the first UIInput element in the large facet. Else find the first UIInput in the small facet.
7. Set the UIInput thus identified to rendered is true, copy the value and readOnly attributes as well as all SelectItem children from the SelectSwitcher element to this UIInput and have the selected facet render itself and its children.
During decode, the SelectSwitcher itself has to do nothing. All of its rendered child components will do their own decode, and thereby intercept a potential new value in the current SelectOne.. element.
Note: in the code snippet above, there are references to a bean. This bean is configured in the faces-config.xml file as follows:
<managed-bean>
<managed-bean-name>DemoSelectSwitcherBean</managed-bean-name>
<managed-bean-class>nl.amis.jsf.beans.DemoSelectSwitcherBean</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
and its implementation is this:
package nl.amis.jsf.beans;
public class DemoSelectSwitcherBean {
Object selectValue;
String switchValue="4";
Boolean readOnly= new Boolean("false");
public DemoSelectSwitcherBean() {
}
public void setSelectValue(Object selectValue) {
this.selectValue = selectValue;
}
public Object getSelectValue() {
return selectValue;
}
public void setSwitchValue(String switchValue) {
this.switchValue = switchValue;
}
public String getSwitchValue() {
return switchValue;
}
public void setReadOnly(Boolean readOnly) {
this.readOnly = readOnly;
}
public Boolean getReadOnly() {
return readOnly;
}
}
The entire JDeveloper Application in which I created and tested this component will be available for download shortly.
Resources
Some resources you may find helpful when it comes to developing custom JSF components:
Exadel’s Tutorial – How to Write your own JSF Components
Richard Hightower on IBM Developer Works: JSF for nonbelievers: JSF component development
Building Custom Java Server Faces UI Components by Chris Schalk (on The Server Side)