9

I use filterFunction method of datatable on primefaces 5.0. I want to filter birthday by date range on column header.

On browser console I receive this error:

<?xml version="1.0"
   encoding="utf-8"?><partial-response><error><error-name>java.lang.ClassCastException</error-name><error-message><![CDATA[javax.faces.component.UIPanel
   cannot be cast to
   javax.faces.component.ValueHolder]]></error-message></error></partial-response>

Datatable :

 <p:dataTable var="person" value="#{testDateRange.persons}"
              id="personTable" paginator="true" styleClass="customTableStyle" editable="true"
              rows="10"  resizableColumns="true"
              emptyMessage="No persons"
              filteredValue="#{testDateRange.filteredPersons}"
              widgetVar="dateRangeWidget" >

     <p:column id="nameId" filterBy="name" sortBy="name" filterMatchMode="contains" headerText="Name">
         <h:outputText value="#{person.name}" />
     </p:column>

     <p:column id="birthdayId" headerText="birthday" filterBy="birthday" filterFunction="#{testDateRange.filterByDate}">
         <f:facet name="filter">
             <p:calendar id="from" value="#{testDateRange.dateFrom}"   styleClass="customCalendar" pattern="dd/MM/yyyy">
                 <p:ajax event="dateSelect" oncomplete="PF('dateRangeWidget').filter()" update="personTable"/>
             </p:calendar>
             <p:calendar id="to" value="#{testDateRange.dateTo}" styleClass="customCalendar" pattern="dd/MM/yyyy">
                 <p:ajax event="dateSelect" oncomplete="PF('dateRangeWidget').filter()" update="personTable"/>
             </p:calendar>
         </f:facet>
         <h:outputText value="#{person.birthday}"  >
             <f:convertDateTime pattern="dd/MM/yyyy"/>
         </h:outputText>
     </p:column>
 </p:dataTable>

Bean:

@Component("testDateRange")
@Scope("session")
public class TestDateRangeBean {

    private List<Person> persons;
    List<Person> filteredPersons;
    private Date dateFrom;
    private Date dateTo;

    public TestDateRangeBean() {
        persons = new ArrayList<>(Arrays.asList(
            new Person("John", new Date(1357016400000L)),
            new Person("Will",new Date(1357102800000L)),
            new Person("Peter",new Date(1414900800000L)),
            new Person("Cris", new Date(1438747200000L)),
            new Person("Cemil", new Date(1436068800000L))
        ));
    }

    public boolean filterByDate(Object value, Object filter, Locale locale) {
        // it fails before calling this method
        String filterText = (filter == null) ? null : filter.toString().trim();
        if(StringUtils.isEmpty(filterText)) {
            return true;
        }
        if(value == null) {
            return false;
        }
        DateFormat df = new SimpleDateFormat("dd/MM/yyyy");
        Date filterDate;
        try {
            filterDate = df.parse(filterText);
        } catch (ParseException e) {
            return false;
        }
        return filterDate.after(dateFrom) &&  filterDate.before(dateTo);
    } 
    //all the getters and setters

enter image description here

And I have a simple POJO Person class that contains only 'name' and 'birthday'.

How can I filter birthday by the dates I enter on column header. I see that it gets the UIPanel component instead of dates values. (I assume it expects one component with a value, and when it finds two components, it returns the container component itself, am I right?)

Thanks

BalusC
  • 992,635
  • 352
  • 3,478
  • 3,452
zekifh
  • 169
  • 1
  • 2
  • 9

4 Answers4

17

Advanced solution:

JSF:

<p:column headerText="#{msg.date}" sortBy="#{bean.date}" filterBy="#{bean.date}" filterFunction="#{dateRangeFilter.filterByDate}">
  <f:facet name="filter">
    <h:inputHidden id="filter" />
  </f:facet>
  <f:facet name="header">
    <h:outputText value="#{msg.date}" />
    <br />
    <p:calendar id="from" pattern="dd.MM.yyyy">
      <p:ajax event="dateSelect" onstart="$(PrimeFaces.escapeClientId('#{p:component('filter')}'))[0].value = $(PrimeFaces.escapeClientId('#{p:component('from')}_input'))[0].value + '-' + $(PrimeFaces.escapeClientId('#{p:component('to')}_input'))[0].value" oncomplete="PF('dataTable').filter()" />
    </p:calendar>
    <p:calendar id="to" pattern="dd.MM.yyyy">
      <p:ajax event="dateSelect" onstart="$(PrimeFaces.escapeClientId('#{p:component('filter')}'))[0].value = $(PrimeFaces.escapeClientId('#{p:component('from')}_input'))[0].value + '-' + $(PrimeFaces.escapeClientId('#{p:component('to')}_input'))[0].value" oncomplete="PF('dataTable').filter()" />
    </p:calendar>
  </f:facet>
  <h:outputText value="#{bean.date}">
    <f:convertDateTime type="date" dateStyle="medium" />
  </h:outputText>
</p:column>

Filter Bean:

@Named
@ApplicationScoped
public class DateRangeFilter implements Serializable {

    private static final Logger LOG = Logger.getLogger(DateRangeFilter.class.getName());

    public boolean filterByDate(Object value, Object filter, Locale locale) {

        String filterText = (filter == null) ? null : filter.toString().trim();
        if (filterText == null || filterText.isEmpty()) {
            return true;
        }
        if (value == null) {
            return false;
        }

        DateFormat df = SimpleDateFormat.getDateInstance(SimpleDateFormat.MEDIUM);

        Date filterDate = (Date) value;
        Date dateFrom;
        Date dateTo;
        try {
            String fromPart = filterText.substring(0, filterText.indexOf("-"));
            String toPart = filterText.substring(filterText.indexOf("-") + 1);
            dateFrom = fromPart.isEmpty() ? null : df.parse(fromPart);
            dateTo = toPart.isEmpty() ? null : df.parse(toPart);
        } catch (ParseException pe) {
            LOG.log(Level.SEVERE, "unable to parse date: " + filterText, pe);
            return false;
        }

        return (dateFrom == null || filterDate.after(dateFrom)) && (dateTo == null || filterDate.before(dateTo));
    }
}
Stephan
  • 186
  • 1
  • 4
  • Thank you for this wonderful suggestio, +1... By the way date parsing could not work for several locales... so, because, you are forcing to save into HTML hidden field in format "dd.MM.yyyy", it's better to initialize the DateFormat in FilterBean using the same format: DateFormat df = new SimpleDateFormat("dd.MM.yyyy"); – Pierpaolo Cira Nov 13 '15 at 15:12
  • This should become part of the next @Primefaces release. Just like `filterMode="date_range"` or `filterMode="dates"`. – Roland Mar 31 '20 at 22:55
16

If you do not want the trickery with the hidden input field plus the benefit of reusing such a filter consider writing a composite component whose component type extends javax.faces.component.UIInput. Primefaces expects the filter to be a subtype of javax.faces.component.ValueHolder.

This is how the composite component might look like.

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
      xmlns:p="http://primefaces.org/ui"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:f="http://java.sun.com/jsf/core"
  xmlns:h="http://java.sun.com/jsf/html">

<!-- INTERFACE -->
<cc:interface componentType="dateRange">
    <cc:attribute name="fromLabel"/>
    <cc:attribute name="toLabel"/>
    <cc:attribute name="value" type="so.example.DateRange"/>
    <cc:attribute name="onvaluechanged"/>
</cc:interface>

<!-- IMPLEMENTATION -->
<cc:implementation>
    <table>
        <tr>
            <td style="width: 15%; border: none;">
                <h:outputText value="#{cc.attrs.fromLabel}"/>
            </td>
            <td style="width: 35%; border: none;">
                <p:calendar id="startDateCalendar" value="#{cc.attrs.value.from}" maxdate="#{cc.attrs.value.to}">
                    <p:ajax event="dateSelect" update="endDateCalendar" oncomplete="#{cc.attrs.onvaluechanged}"/>
                </p:calendar>
            </td>
            <td style="width: 15%; border: none;">
                <h:outputText value="#{cc.attrs.toLabel}"/>
            </td>
            <td style="width: 35%; border: none;">
                <p:calendar id="endDateCalendar" value="#{cc.attrs.value.to}" mindate="#{cc.attrs.value.from}" maxdate="#{cc.attrs.value.now}">
                    <p:ajax event="dateSelect" update="startDateCalendar" oncomplete="#{cc.attrs.onvaluechanged}"/>
                </p:calendar>
            </td>
        </tr>
    </table>
</cc:implementation>

Notice componentType="dateRange" in the cc:interface tag. This references the root component class of this composite component. Which is as simple as.

@FacesComponent("dateRange")
public class DateRangeComponent extends UIInput implements NamingContainer {

    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

}

The value that the composite component takes is a simple POJO.

public class DateRange implements Serializable {

    private Date from;
    private Date to;
    private boolean ignoreTime = true;

    public Date getFrom() {
        return from;
    }

    public void setFrom(Date from) {
        if (this.isIgnoreTime()) {
            Calendar now = Calendar.getInstance();
            now.setTime(from);
            now.set(Calendar.HOUR_OF_DAY, 0);
            now.set(Calendar.MINUTE, 0);
            now.set(Calendar.SECOND, 0);
            this.from = now.getTime();
        } else {
            this.from = from;
        }
    }

    public Date getTo() {
        return to;
    }

    public void setTo(Date to) {
        if (this.isIgnoreTime()) {
            Calendar now = Calendar.getInstance();
            now.setTime(to);
            now.set(Calendar.HOUR_OF_DAY, 23);
            now.set(Calendar.MINUTE, 59);
            now.set(Calendar.SECOND, 59);
            this.to = now.getTime();
        } else {
            this.to = to;
        }
}

public Date getNow() {
    return new Date();
}

public boolean isIgnoreTime() {
    return ignoreTime;
}

public void setIgnoreTime(boolean ignoreTime) {
    this.ignoreTime = ignoreTime;
}
}

After all that the usage is very simple.

            <p:column headerText="#{labels.date}"
                      sortBy="#{logIn.loginDate}"
                      filterBy="#{logIn.loginDate}"
                      filterFunction="#{logInTableBean.filterByDate}"
                      styleClass="datetime-column">
                <f:facet name="filter">
                    <clx:dateRange fromLabel="#{labels.from}" 
                                   toLabel="#{labels.to}" 
                                   onvaluechanged="PF('logInTable').filter();"
                                   value="#{logInTableBean.range}"/>
                </f:facet>
                <h:outputText value="#{logIn.loginDate}">
                    <f:convertDateTime type="both" dateStyle="long"/>
                </h:outputText>
            </p:column>

Also notice the custom filter function. Whis is as simple as

public boolean filterByDate(Object value, Object filter, Locale locale) {
    Date colDate = (Date) value;
    return this.range.getFrom().before(colDate) && this.range.getTo().after(colDate);
}

The whole thing will look like this.

daterangefilter

FuryFart
  • 2,083
  • 3
  • 22
  • 39
  • One question, in the usage part, there is this ` – blurfus Mar 24 '15 at 17:44
  • 2
    @ochi it is ia randomly picked namespace in this case `xmlns:clx="http://xmlns.jcp.org/jsf/composite/clxcomponents"`. For a quick introduction on how to work with composite components you can read this [blog post](http://balusc.blogspot.de/2013/01/composite-component-with-multiple-input.html) – FuryFart Mar 25 '15 at 08:03
  • Suggested component works fine, thanks. The problem in my case is that although `dateRange` is subtype of `ValueHolder`, value is not sent to the filter. When I change type of value to string, it works. How can I send dateRange value to the filter? – Micer Sep 17 '15 at 07:48
  • Hi Surprised Coconut. Thanks for your contribution. I tried to use this component on a datatable with lazy loading but I don't know how to deal with value attribute. Do you know how I can retrieve the selected object on filter map at load method ? – André G. Andrade Aug 19 '16 at 05:14
  • As I used filter without filterFunction, I had to override getLocalValue to return an instance of DateRange when super.getLocalValue() is null. – w35l3y Dec 13 '17 at 11:48
  • I find ``, `` and `
    ` very static and may not be responsive?
    – Roland Mar 31 '20 at 22:57
  • @Roland Yes of course. You can use whatever HTML you want for layout. – FuryFart Apr 06 '20 at 19:25
3

Encountered same problem but in my case it was button in filter facet to show overlay panel with range slider inside.

To solve it use header facet instead:

    <f:facet name="filter">
         <!-- to hide default filter input -->
         <h:inputHidden />
    </f:facet>
    <f:facet name="header">
         <p:outputLabel value="birthday" /><br />
         <p:calendar id="from" value="#{testDateRange.dateFrom}"   styleClass="customCalendar" pattern="dd/MM/yyyy">
             <p:ajax event="dateSelect" oncomplete="PF('dateRangeWidget').filter()" />
         </p:calendar>
         <p:calendar id="to" value="#{testDateRange.dateTo}" styleClass="customCalendar" pattern="dd/MM/yyyy">
             <p:ajax event="dateSelect" oncomplete="PF('dateRangeWidget').filter()" />
         </p:calendar>
     </f:facet>

Also there is no need in update attribute in p:ajax components.

alexSunder
  • 418
  • 3
  • 10
  • When I use `` in filter, I get `java.lang.ClassCastException: javax.faces.component.UIPanel cannot be cast to javax.faces.component.ValueHolder` and the filter does not work. I suppose h:inputHidden cannot be in filter. – Micer Sep 11 '15 at 08:16
  • 2
    Okay, the problem was in comment. It needs to be either removed or put into remove tag: ``. – Micer Sep 11 '15 at 08:23
1

This is just Java 8 implementation of @Stephan's answer, using java.time class instead of Date.

The JSF section is almost identical except the date pattern.

<p:column headerText="Date" filterBy="#{passbook.createdAt}" filterFunction="#{applicationController.filterByDate}">
<f:facet name="filter">
    <h:inputHidden id="filter" />
</f:facet>
<f:facet name="header">
    <h:outputText value="Date" />
    <br />
    <p:calendar id="from" pattern="dd-MMM-yyyy" size="12">
        <p:ajax event="dateSelect" onstart="$(PrimeFaces.escapeClientId('#{p:component('filter')}'))[0].value = $(PrimeFaces.escapeClientId('#{p:component('from')}_input'))[0].value + '>' + $(PrimeFaces.escapeClientId('#{p:component('to')}_input'))[0].value" oncomplete="PF('passbookTable').filter()" />
    </p:calendar>
    <h:outputText class="fa fa-arrows-h fa-2x" style="vertical-align: top;"/>
    <p:calendar id="to" pattern="dd-MMM-yyyy" size="12">
        <p:ajax event="dateSelect" onstart="$(PrimeFaces.escapeClientId('#{p:component('filter')}'))[0].value = $(PrimeFaces.escapeClientId('#{p:component('from')}_input'))[0].value + '>' + $(PrimeFaces.escapeClientId('#{p:component('to')}_input'))[0].value" oncomplete="PF('passbookTable').filter()" />
    </p:calendar>
</f:facet>
<h:outputText value="#{passbook.createdAt}">
    <f:convertDateTime type="date" dateStyle="medium" pattern="dd-MMM-yyyy"/>
</h:outputText>

Bean:

@Named
@ApplicationScoped
public class ApplicationController implements Serializable {
    private static final Logger logger = LogManager.getLogger(ApplicationController.class.getName());

public boolean filterByDate(Object value, Object filter, Locale locale) {
        String filterText = (filter == null) ? null : filter.toString().trim();
        if (filterText == null || filterText.isEmpty()) {
            return true;
        }
        if (value == null) {
            return false;
        }

        DateTimeFormatter sdf = DateTimeFormatter.ofPattern("dd-MMM-yyyy");
        Instant instant = ((Date) value).toInstant(); //Zone : UTC+0

        LocalDate dateValue = instant.atZone(ZoneId.of("Asia/Kolkata")).toLocalDate();
        LocalDate dateFrom;
        LocalDate dateTo;
        try {
            String fromPart = filterText.substring(0, filterText.indexOf(">"));
            String toPart = filterText.substring(filterText.indexOf(">") + 1);
            dateFrom = fromPart.isEmpty() ? null : LocalDate.parse(fromPart, sdf);
            dateTo = toPart.isEmpty() ? null : LocalDate.parse(toPart, sdf);
        } catch (Exception e) {
            logger.error("unable to parse date: " + filterText, e);
            return false;
        }

        return (dateFrom == null || dateValue.isAfter(dateFrom) || dateValue.isEqual(dateFrom))
                && (dateTo == null || dateValue.isBefore(dateTo) || dateValue.isEqual(dateTo));
    }
PC.
  • 133
  • 1
  • 12