Introduction
Composite components are a nice JSF2/Facelets feature. As stated in this stackoverflow.com answer, you can use it to create a reuseable component with a single responsibility based on existing JSF components and/or HTML.
Use Composite Components if you want to create a single and reuseable custom UIComponent
with a single responsibility using pure XML. Such a composite component usually consists of a bunch of existing components and/or HTML and get physically rendered as single component. E.g. a component which shows a rating in stars based on a given integer value. An example can be found in our Composite Component wiki page.
The wiki page contains however only an example of a composite component with a pure output function (showing a rating in stars). Creating a composite component based on a bunch of closely related UIInput
components is a little tougher, but it's not demonstrated in the wiki page. So, let's write a blog about it.
Back to top
Bind java.util.Date value to 3 day/month/year dropdowns
Although the calendar popup is becoming more popular these days, a not uncommon requirement is to have a date selection by three dropdown lists representing the day, month and year. In JSF terms, you'd thus need three <h:selectOneMenu>
components and a little bit of ajax or even plain vanilla JavaScript in order to get the days right depending on the selected month and year. Not every month has the same amount of days and a particular month has even a different amount of days depending on the year.
Let's start with some XHTML first:
<ui:component
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:cc="http://java.sun.com/jsf/composite"
>
<cc:interface componentType="inputDate">
<cc:attribute name="value" type="java.util.Date"
shortDescription="The selected Date. Defaults to today." />
<cc:attribute name="maxyear" type="java.lang.Integer"
shortDescription="The maximum year. Defaults to current year." />
<cc:attribute name="minyear" type="java.lang.Integer"
shortDescription="The minimum year. Defaults to maxyear minus 100." />
</cc:interface>
<cc:implementation>
<span id="#{cc.clientId}" style="white-space:nowrap">
<h:selectOneMenu id="day" binding="#{cc.day}" converter="javax.faces.Integer">
<f:selectItems value="#{cc.days}" />
</h:selectOneMenu>
<h:selectOneMenu id="month" binding="#{cc.month}" converter="javax.faces.Integer">
<f:selectItems value="#{cc.months}" />
<f:ajax execute="day month" listener="#{cc.updateDaysIfNecessary}" />
</h:selectOneMenu>
<h:selectOneMenu id="year" binding="#{cc.year}" converter="javax.faces.Integer">
<f:selectItems value="#{cc.years}" />
<f:ajax execute="day year" listener="#{cc.updateDaysIfNecessary}" />
</h:selectOneMenu>
</span>
</cc:implementation>
</ui:component>
Save it as /resources/components/inputDate.xhtml
.
The componentType
attribute of the <cc:interface>
tag is perhaps new to you. It basically allows you to bind the composite component to a so-called backing component. This must be an instance of UIComponent
and implement at least the NamingContainer
interface (as required by the JSF composite component specification). Given that we basically want to create an input component, we'd like to extend from UIInput
. The component type inputDate
represents the component type and should be exactly the same value as is been declared in the value of the @FacesComponent
annotation. The concrete backing component instance is available by the implicit EL variable #{cc}
inside the <cc:implementation>
.
The three <h:selectOneMenu>
components are all via binding
attribute bound as UIInput
properties of the backing component which allows easy access to the submitted values and the (local) values. The <f:selectItems>
also obtains all available values from the backing component. The <f:ajax>
listener is also declared in the backing component. The enduser has only to provide a java.util.Date
property as composite component value. The backing component does all the heavy lifting job.
Oh, there's also a <span>
with the client ID of the composite component. This allows easy referencing in ajax updates from outside as follows:
<my:inputDate id="foo" ... />
...
<f:ajax ... render="foo" />
The composite component is by its own client ID available in the JSF component tree and thus accessible for ajax updates, but this client ID is by default nowhere represented by a HTML element and thus JavaScript wouldn't be able to find it in the HTML DOM (via document.getElementById()
and so on) in order to update the HTML representation. So you need to supply your own HTML representation. This is in detail explained in the following stackoverflow.com questions:
- Referring to a Composite Component from a Using Page Fails
- JSF update with ajax non JSF things
- Integrate JavaScript with JSF, the clean way
Back to top
Backing component of the composite component
Here's the necessary Java code!
package com.example;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import javax.faces.component.FacesComponent;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIInput;
import javax.faces.component.UINamingContainer;
import javax.faces.context.FacesContext;
import javax.faces.convert.ConverterException;
import javax.faces.event.AjaxBehaviorEvent;
@FacesComponent("inputDate")
public class InputDate extends UIInput implements NamingContainer {
// Fields -------------------------------------------------------------------------------------
private UIInput day;
private UIInput month;
private UIInput year;
// Actions ------------------------------------------------------------------------------------
/**
* Returns the component family of {@link UINamingContainer}.
* (that's just required by composite component)
*/
@Override
public String getFamily() {
return UINamingContainer.COMPONENT_FAMILY;
}
/**
* Set the selected and available values of the day, month and year fields based on the model.
*/
@Override
public void encodeBegin(FacesContext context) throws IOException {
Calendar calendar = Calendar.getInstance();
int maxYear = getAttributeValue("maxyear", calendar.get(Calendar.YEAR));
int minYear = getAttributeValue("minyear", maxYear - 100);
Date date = (Date) getValue();
if (date != null) {
calendar.setTime(date);
int year = calendar.get(Calendar.YEAR);
if (year > maxYear || minYear > year) {
throw new IllegalArgumentException(
String.format("Year %d out of min/max range %d/%d.", year, minYear, maxYear));
}
}
day.setValue(calendar.get(Calendar.DATE));
month.setValue(calendar.get(Calendar.MONTH) + 1);
year.setValue(calendar.get(Calendar.YEAR));
setDays(createIntegerArray(1, calendar.getActualMaximum(Calendar.DATE)));
setMonths(createIntegerArray(1, calendar.getActualMaximum(Calendar.MONTH) + 1));
setYears(createIntegerArray(maxYear, minYear));
super.encodeBegin(context);
}
/**
* Returns the submitted value in dd-MM-yyyy format.
*/
@Override
public Object getSubmittedValue() {
return day.getSubmittedValue()
+ "-" + month.getSubmittedValue()
+ "-" + year.getSubmittedValue();
}
/**
* Converts the submitted value to concrete {@link Date} instance.
*/
@Override
protected Object getConvertedValue(FacesContext context, Object submittedValue) {
try {
return new SimpleDateFormat("dd-MM-yyyy").parse((String) submittedValue);
}
catch (ParseException e) {
throw new ConverterException(e); // This is not to be expected in normal circumstances.
}
}
/**
* Update the available days based on the selected month and year, if necessary.
*/
public void updateDaysIfNecessary(AjaxBehaviorEvent event) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.DATE, 1);
calendar.set(Calendar.MONTH, (Integer) month.getValue() - 1);
calendar.set(Calendar.YEAR, (Integer) year.getValue());
int maxDay = calendar.getActualMaximum(Calendar.DATE);
if (getDays().length != maxDay) {
setDays(createIntegerArray(1, maxDay));
if ((Integer) day.getValue() > maxDay) {
day.setValue(maxDay); // Fix the selected value if it exceeds new max value.
}
FacesContext context = FacesContext.getCurrentInstance(); // Update day field.
context.getPartialViewContext().getRenderIds().add(day.getClientId(context));
}
}
// Helpers ------------------------------------------------------------------------------------
/**
* Return specified attribute value or otherwise the specified default if it's null.
*/
@SuppressWarnings("unchecked")
private <T> T getAttributeValue(String key, T defaultValue) {
T value = (T) getAttributes().get(key);
return (value != null) ? value : defaultValue;
}
/**
* Create an integer array with values from specified begin to specified end, inclusive.
*/
private static Integer[] createIntegerArray(int begin, int end) {
int direction = (begin < end) ? 1 : (begin > end) ? -1 : 0;
int size = Math.abs(end - begin) + 1;
Integer[] array = new Integer[size];
for (int i = 0; i < size; i++) {
array[i] = begin + (i * direction);
}
return array;
}
// Getters/setters ----------------------------------------------------------------------------
public UIInput getDay() {
return day;
}
public void setDay(UIInput day) {
this.day = day;
}
public UIInput getMonth() {
return month;
}
public void setMonth(UIInput month) {
this.month = month;
}
public UIInput getYear() {
return year;
}
public void setYear(UIInput year) {
this.year = year;
}
public Integer[] getDays() {
return (Integer[]) getStateHelper().get("days");
}
public void setDays(Integer[] days) {
getStateHelper().put("days", days);
}
public Integer[] getMonths() {
return (Integer[]) getStateHelper().get("months");
}
public void setMonths(Integer[] months) {
getStateHelper().put("months", months);
}
public Integer[] getYears() {
return (Integer[]) getStateHelper().get("years");
}
public void setYears(Integer[] years) {
getStateHelper().put("years", years);
}
}
The backing component instance has basically a lifetime of exactly one HTTP request. This means that it's recreated on every single HTTP request, like as a request scoped managed bean. So if you ever manually create variables during an encodeXxx()
method which you'd like to be available in any of the component's methods during the subsequent postback request (the form submit), then you should not be assigning it as a field of the class. It would get lost by end of initial request and reinitialize to default (e.g. null
) during the postback request.
If you've ever developed a custom UIComponent
, or looked in the source code of an existing UIComponent
, then you have probably already seen the StateHelper
which is available by the inherited getStateHelper()
method. This basically takes care about the component's state across postbacks. It has basically the same lifetime as a view scoped managed bean. You can use the put()
method to store a variable in the component's state. You can use the get()
or eval()
method to get or EL-evaluate a variable from the component's state. In this particular backing component, this is done so for the <f:selectItems>
dropdown values. Look at their getters/setters, they all delegate directly to StateHelper
.
This is not done so for the UIInput
properties which represents each of the <h:selectOneMenu> components. JSF will namely already automatically set them via binding
attribute during building/restoring of the view. Even more, you're not supposed to save complete UIComponent
instances in component's state. Note that you can also use e.g. UIInput day = (UIInput) findComponent("day");
instead of binding="#{cc.day}"
with a day
property, but this may result in some boilerplate code as you need this in multiple methods.
When the component is about to be rendered, the encodeBegin()
method is invoked which basically obtains the maxyear
, minyear
and value
attributes and initializes the dropdowns. The first two attributes represent the maximum and minimum value of the "year" dropdown, which in turn defaults to respectively the current year and the maximum year minus 100. The input value is as per <cc:attribute type>
already expected to be an instance of java.util.Date
. Note that the value is obtained by the getValue()
method which is inherited from UIInput
. After a simple max/min year check, the individual day, month and year fields are obtained from the calendar and set as values of dropdown components. Finally the available values of the dropdown components are filled.
When the form is submitted and the request values have been applied (which is basically what the decode()
method of the input component should be doing, but as we're delegating it to the three dropdown components, we actually don't need to override anything here), the getSubmittedValue()
method will be invoked in order to obtain the "raw" submitted value which is used for the usual conversion/validation steps. The backing component will return the submitted value as a string in dd-MM-yyyy
format. It's important that this value is not null
, otherwise JSF will skip the conversion/validation/modelupdate. If you happen to use MyFaces instead of Mojarra, then you need to replace getSubmittedValue()
call on child component by getValue()
:
@Override
public Object getSubmittedValue() {
return day.getValue()
+ "-" + month.getValue()
+ "-" + year.getValue();
}
Or, if you'd like to cover both:
@Override
public Object getSubmittedValue() {
return (day.getSubmittedValue() == null && day.isLocalValueSet() ? day.getValue() : day.getSubmittedValue())
+ "-" + (month.getSubmittedValue() == null && month.isLocalValueSet() ? month.getValue() : month.getSubmittedValue())
+ "-" + (year.getSubmittedValue() == null && year.isLocalValueSet() ? year.getValue() : year.getSubmittedValue());
}
Here, the child component is first checked if it has no submitted value and has its local value set, and if that's the case, then return its local value instead of the submitted value. This is needed because Mojarra and MyFaces don't agree on whether to process the UIInput
component itself first before processing its children, or the other way round. Mojarra first processes the UIInput
component itself before its children, and therefore needs getSubmittedValue()
. MyFaces, on the other hand, first processes the children before the UIInput
component itself, and therefore needs getValue()
.
Shortly after getting the submitted value, JSF will invoke the getConvertedValue()
method, passing exactly the submitted value as 2nd argument. Normally this method is not to be overridden and everything is delegated to default JSF Converter
mechanisms, but the backing component has it overriden to take the opportunity to convert the submitted value to a concrete java.util.Date
instance which will ultimately be updated in the model.
Note that no validation is performed and that's not necessary, because it's impossible for a hacker to provide a different submitted value than shown in the dropdowns (e.g. a day of 33). In any attempt, JSF would simply fail the usual way with Validation Error: Value is not valid
on the associated dropdown.
Finally, there's a "proprietary" ajax action listener method updateDaysIfNecessary()
which should update the day dropdown depending on the value of the month and year dropdowns, if necessary. It basically determines the maximum day of the given month and year and checks if the available days and currently selected day needs to be altered if the maximum day has been changed. If that's the case, then a programmatic ajax render will be instructed by adding the client ID of the day dropdown to PartialViewContext#getRenderIds()
.
Usage example
Here's how you can use it in an arbitrary form. First declare the composite component's XML namespace in the top level XML element:
xmlns:my="http://java.sun.com/jsf/composite/components"
The prefix "my
" is fully to your choice. The /components
part of the path is also fully to your choice, it's basically the name of the subfolder in the /resources
folder where you've placed the composite component XHTML file. Given this XML namespace, it's thus available as <my:inputDate>
as follows:
<h:form>
<my:inputDate value="#{bean.date1}" /><br />
<my:inputDate value="#{bean.date2}" maxyear="2050" /><br />
<my:inputDate value="#{bean.date3}" maxyear="2000" minyear="1990" /><br />
<h:commandButton value="Submit" action="#{bean.submit}" />
</h:form>
The date1
, date2
and date3
properties are all of type java.util.Date
. Nothing special, it can even be null
, it would default to today's date anyway. See also this little video demo of how the component behaves in the UI.
41 comments:
hey, BalusC!
Great article, as usual!
I'm always learning from you, pal. :)
Question: in the updateDays method, you use:
context.getPartialViewContext().getRenderIds().add(day.getClientId(context));
Wouldn't suffice to use render="day" in the f:ajax?
Yes, that would also work just fine. But sometimes that's simply not necessary. E.g. when switching from January to March. Nothing needs to be changed. You'd like to avoid unnecessary renders.
Oh, of course, I see now!
Neat, thanks! :)
When using OmniFaces, this is by the way exactly what Ajax#update() method is doing.
Nice Post!
I'm using a very similar composite component (with a SelectOneMenu component). The main difference is, that I'm using my composite component nested in another composite component.
I get an Exception "PropertyNotFoundException target unreachable nul" when the UIInput.updateModel() method is called. It seems, it cannot evaluate an expression like "cc.attrs.bean.beanProperty" that was set in the composite component in which my component is nested.
Do you have any idea how to solve that?
Regarding my last comment. I could find out more about the problem.
It seems that UIInput.updateModel() is called twice! The first call is executed successfully, which means that the specific target could be reached. The second call throws de exception.
We are using primefaces. It seems that a primefaces component (Dialog) is triggering the second call to updateModels
I think there's a little bug in the code.
setMonths(createIntegerArray(1, calendar.getActualMaximum(Calendar.MONTH) +1));
Now I got 12 months in the combobox.
Very nice and enlightening article.
@Werner: fixed, thanks :)
Good article!
Say I want to show the month's names instead of numbers on the selectOneMenu.
Should I tinker with the InputDate class a little, or is there some better way?
PS: Many times your anwers on SO have helped me, you rock. :)
@Rony: replace Integer[] months by Map<String, Integer> months.
Using a Map worked like a charm.
Thanks Bauke!
Can u please send me sample project with the above component.
Regards,
Sandeep Raju
@Sandeep: no, I won't. The code provided in the blog is already complete and copy'n'paste'n'runnable.
Hi,
thanks for this, it was just what I was looking for and helped a great deal getting started with own input components.
I only had the error that getConvertedValue ran into an exception as getSubmittedValue() returned "null-null-null" always.
I found out that I had to replace day.getSubmittedValue() etc. to day.getLocalValue() in the provided getSubmittedValue() method.
After this it works like intended for me.
Thanks Bauke for this helpful tutorial. I have one question: I would to extend the inputDate.xhtml, so that for day/month/year has
also one f:selectItem like this for month f:selectItem itemLabel="#{bundle['label.register.page.monthofbirth']}" noSelectionOption="true".
But this seem not to work. See below my change in inputDate.xhtml.
Hi BalusC.
I love your contributions on JSF knowledge. I have a cuestion for you
Can I handle button action in facelets custom tags? (not custom component, not composite component)
...
and calls it:
Thanks so much
Yes. Great article and works like a charm.
But I noticed some problem when for instance one of the input fields is just a text (without a converter assigned). And now "getConvertedValue" throws a ConverterException because the whole thing is invalid.
The newly entered text isn't kept but reset to its former value. Why is that? And what can I do about that?
Actually the problem is not because of using a converter or not. It's just that the values are reset when a converter exception is thrown in getConvertedValue.
I'm using something similar to this method (myfaces 2.0.18) and I noticed that the selectBooleanCheckbox component values weren't being saved when an ajax event occurred. I traced the problem down to the "cc" attribute not being resolved in the processValidators method stack of my custom class. This lead me to discover that there is a difference in implementation of processValidators between UIInput and UIComponentBase. In the UIComponentBase, the el variables are setup (pushComponentToEL) for the duration of iterating through facets and children while in UIInput, the facets and children are iterated outside the el context. The solution that seems to work for me is to override processValidators like this:
public void processValidators( FacesContext context )
{
try
{
pushComponentToEL( context, this );
super.processValidators( context );
}
finally
{
popComponentFromEL( context );
}
}
Do you think this is a myfaces issue or maybe we need to extend from a different class than UIInput?
Hi BalusC,
Could you please help me in
http://stackoverflow.com/questions/19257142/unable-to-use-poverlaypanel-inside-composite-component
can we change the slider in the time picker to a text box entry?
My requirement is to type in the time to the calendar. like a editable drop down where we can enter from 0-12 and another from 0-59
When setting the child components to "invalid" in case of an exception it works for me. Probably not the best solution but it seems to work.
First off thanks for this indepth blog on composite component.
IHAQ regarding ui:component. I've always created my composite component without the ui:component outer tag. What's the difference of having the ui:component tag?
Thanks
@Chuk: JSF implicitly uses <ui:component> for composites. So just using it directly is cleaner than <ui:composition> or even <html>.
Very good exapmle,
But is is possible to add a change event to this component? I need to use an ajax call when the value of date changes.
So that i can reackt with f:ajax on an change event and render something else or save the value with an ajax call into the bean attribute.
BalusC i repleace Integer[] with Map for reason that it show me month name. it works. but Order of month is not true. how can i solve it? is there a solution?
thanks for your attention and your good article
HashMap is ordered by hashcode, not by insertion order. Use LinkedHashMap to order by insertion order like ArrayList.
Very Very ... Thanks. ;)
Hi,
Excuseme BalusC i have a new problem. i change your java code and remove all calendar. i create a new class with name CustomDate and int it define 3 int varable as year, month and day.
in the backing bean i replace CustomDate with Date.
when i call a xhtml page i work good. but when i submit a form it throw a exception and say can not cast Date to CustomDate.
do you know where is problem?
This article rocks. As usual.
Thanks !
Hey, BalusC!
Great article!
Question:
how to add atributes onclick and onchange to cc?
Hi BalusC,
I've a few questions...
This is something basic, but I can't get it:
this should submit the whole page, am I wrong? In your video it's shown that only necessary fields are updated.
I tried to change it to
but I have now the following problem, I receive an exception 'form:input: Conversion error occurred.' What am I doing wrong? I basically copied your code one to one and just trying to run. Is anywhere working example of your code where I can it download? Thank you in advance.
Sorry, It looks that it doesn't accept to post code, I put the whole question here: http://pastebin.com/hC73R8Ng Thank you
Ok, I can confirm that your code doesn't work with WAS 8.0.0.9, it has custom JSF implementation based on Apache MyFaces. As I said earlier it throws error 'BeanValidator W cannot validate component with empty value: form:range_chooser:hiddenValue'. When I tried to implement it at home on WildFly 8.2 + Apache MyFaces 2.2, everything worked fine. I suspect that it's related to this bug, will try to implement solution given in topic above : http://stackoverflow.com/questions/3933786/jsf-2-bean-validation-validation-failed-empty-values-are-replaced-with-las
I must admit that composites yet again confuse me. Initially I thought that CC is yet another template engine eventually and long awaited to be designed the proper way. Unfortunately my hopes have been ripped apart by trying to have a "composite" table column.
Also this blog post confuses me: why am I forced to write again some Java code, i.e. a backing bean of a component? Why can't this be done on a XML scripting level like the layout itself????
BalusC,
Is there any way to initialize a composite component in a way similar to f:viewAction?
For example, I want to render a toplist with a particular ID. I'd like to pass the ID as the ui:param inside the ui:include.
Is it possible to do this?
Thanks,
V.
Hi BalusC,
My challenge is totally different. I have multiple h:input tags in my jsf page. And I want to create an object with each and every h:input tag. To do so I am calling a method with static parameter (this method checks whether any row exists in database with the given static parameter, if yes it fetches the value.)
But my challenge is, if there is no record available with the passed static parameter the how should I get the new value entered by the user.
It would be grateful if you can help.
Thanking you in the meantime.
Hi BalusC,
I tried to implement your example, but when I click the button in the Bean is null the properties date1, date2, date3...I initialize those properties but those took the value of bean and not the value of the component
Hi BalusC, what would be the correct way to reuse your component to make a period component (which takes a startDate and a endDate)?
In your component, you override the getSubmittedValue() and return a formatted string ("dd-MM-yyyy"). But how should I return the value for a period? Do I absolutely need to return something as a String (let's say in a "dd-MM-yyyy,dd-MM-yyyy" format + have the corresponding converter) or is it possible to construct the period object within the component with "new Period(startDate.getValue(), endDate.getValue())" (and what method should return it (getValue()) and how would validation work) ?
Thanks
Hi BalusC
From this example, I am trying to have a minDate and a maxDate. Therefore, when I select the year of my minDate (or maxDate) both month and day fields must be updated. Therefore, if my minDate = 1946-December-07, and I choose year 1946, the months should be only december and days should be 7 to 31. When in my inpuDate.java, the value coming of getConvertedValue(FacesContext context, Object submittedValue) is the correct date (7th of December 1946). But this value never seems to reach my ManagedBean. Thier must be an error somewhere but I cannot figure it out. What happens after the getConvertedValue()? What could prevent the sumit of the Date object?.
Thanks
Hi BalusC, thank for this article. But trying to implement this, i'm run into a problem, with mojarra 2.2.13-SP1 in Wildfly 10 the updateDaysIfNecessary listener never get called. I've tried to add event attribute with change value to f:ajax, but nothing happens.
Best regards.
Post a Comment