Tuesday, January 15, 2013

Composite component with multiple input fields

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:

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.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. 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.

34 comments:

eljunior said...

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?

Bauke Scholtz said...

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.

elias said...

Oh, of course, I see now!
Neat, thanks! :)

Bauke Scholtz said...

When using OmniFaces, this is by the way exactly what Ajax#update() method is doing.

Carlos said...

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?

Carlos said...

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

Werner said...

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.

Bauke Scholtz said...

@Werner: fixed, thanks :)

Rony Clay said...

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. :)

Bauke Scholtz said...

@Rony: replace Integer[] months by Map<String, Integer> months.

Rony Clay said...

Using a Map worked like a charm.
Thanks Bauke!

Sandeep Raju said...

Can u please send me sample project with the above component.

Regards,
Sandeep Raju

Bauke Scholtz said...

@Sandeep: no, I won't. The code provided in the blog is already complete and copy'n'paste'n'runnable.

Kphor said...

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.

serge.pagop said...

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.

Anil's Rubbish said...
This comment has been removed by the author.
Anil's Rubbish said...

This has been really great. However, I was wondering if you knew of a good way to propagate the change/valueChange events to that I could use an f:ajax tag within the my:inputDate tag so that I can render dynamically an outside div or something. I've been trying to figure that out for the last day or so, but it seems like I'm missing something here.
Thanks

Juan César Farro Romero said...

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

tdtappe said...

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?

tdtappe said...

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.

Jamie said...

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?

haunting said...

Hi BalusC,

Could you please help me in

http://stackoverflow.com/questions/19257142/unable-to-use-poverlaypanel-inside-composite-component

Dileep Gopalakrishnan said...

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

Heiko Tappe said...

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.

Chuk Lee said...

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

Bauke Scholtz said...

@Chuk: JSF implicitly uses <ui:component> for composites. So just using it directly is cleaner than <ui:composition> or even <html>.

Gerhard Totz said...

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.

فرزند ایران said...

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

Bauke Scholtz said...

HashMap is ordered by hashcode, not by insertion order. Use LinkedHashMap to order by insertion order like ArrayList.

فرزند ایران said...
This comment has been removed by the author.
فرزند ایران said...

Very Very ... Thanks. ;)

فرزند ایران said...

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?

A.Dumeige said...

This article rocks. As usual.
Thanks !

mbanovic said...

Hey, BalusC!
Great article!

Question:
how to add atributes onclick and onchange to cc?