Thursday, June 24, 2010

JSF composite component

On my latest project we used RichFaces as a component library. Our client didn't like to fill out the time with the RichFaces calendar. They wanted two combo boxes to fill out the time. I don't think this is the most user friendly way to solve picking a time (I like this timepicker) but our customer is king.

I thought that there would be someone on the internet who already created a component like that, but I could not find any so I made my own. JSF 2 will introduce a new component creation method. It will be simpler to create components and you do not have to write java code to make them work. But until we have JSF 2 available on all major application servers we will have to program some java for our components to work.

The plan I had for creating it was simple enough just take two HtmlSelectOneMenu components and fill them with default options.

So I ended with something like this:


private void createChildComponents(FacesContext context) {
Application application = context.getApplication();
hourInput = (HtmlSelectOneMenu) application.createComponent(HtmlSelectOneMenu.COMPONENT_TYPE);
minuteInput = (HtmlSelectOneMenu) application.createComponent(HtmlSelectOneMenu.COMPONENT_TYPE);
List<UIComponent> children = getChildren();
children.add(hourInput);
HtmlOutputText sparator = (HtmlOutputText) application.createComponent(HtmlOutputText.COMPONENT_TYPE);
sparator.setValue(":");
children.add(sparator);
children.add(minuteInput);

UISelectItems hourItems = createSelectItems(application, 24, 1);
hourInput.getChildren().add(hourItems);

UISelectItems minuteItems = createSelectItems(application, 60, 5);
minuteInput.getChildren().add(minuteItems);
delegateProperties();
}

private UISelectItems createSelectItems(Application application, int number, int step) throws FacesException {
UISelectItems selectItems = (UISelectItems) application.createComponent(UISelectItems.COMPONENT_TYPE);
List<SelectItem> items = new ArrayList<SelectItem>();
for (int i = 0; i < number; i += step) {
items.add(new SelectItem(StringUtils.leftPad(String.valueOf(i), 2, "0")));
}
selectItems.setValue(items);
return selectItems;
}


Rendering this was not hard but to extract the selected value and store the submitted value back into the value binding, that was not so easy. First I tried to override the validate method but that didn't "sound" right, doing it in the updateModel was much better.

The only weird thing I was still facing was that, sometimes the value from the hour and minute components were converted to Integer but sometimes they where not. So I ended up with this construction to ensure that they were always Integer.


@Override
public void updateModel(FacesContext context) {
super.updateModel(context);

hourInput.validate(context);
minuteInput.validate(context);

ValueExpression valueExpression = getValueExpression("value");
Date value = (Date) valueExpression.getValue(context.getELContext());
if (value != null && hourInput.getValue() != null
&& minuteInput.getValue() != null) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(value);
Integer hour = Integer.valueOf(hourInput.getValue().toString());
calendar.set(Calendar.HOUR_OF_DAY, hour);
Integer minute = Integer.valueOf((minuteInput.getValue().toString()));
calendar.set(Calendar.MINUTE, minute);

valueExpression.setValue(context.getELContext(), calendar.getTime());
}
}


Here I'm using the Java Date API. That of course is extremely sadistic so we could change it to use joda-time.

Now to use it we first register it as a component in components.taglib.xml

<facelet-taglib>
<namespace>http://ctp-consulting.com/components</namespace>
<!--
usage: <ctp:timepicker value="date"/>
-->
<tag>
<tag-name>timepicker</tag-name>
<component>
<component-type>com.ctp.web.components.Timepicker</component-type>
</component>
</tag>


Then make sure you have the components.taglib.xml registered in you're web.xml

<context-param>
<param-name>facelets.LIBRARIES</param-name>
<param-value>/WEB-INF/tags/components.taglib.xml;</param-value>
</context-param>

And here an example of use, fist add the namespace on the top of you're page:

xmlns:ctp="http://ctp-consulting.com/components"

Then in our page you can do something like:

<ctp:timepicker value="#{catering.deliveryTime}"/>

Maybe this is not the best solution, if you have comments or tips, feel free drop them directly on this post. I'm interested in what you have to say!

No comments: