p:dataTable footer facet doesn't re-render on filter change

UI Components for JSF
Post Reply
tpacker
Posts: 3
Joined: 21 Oct 2011, 01:18

21 Oct 2011, 01:56

We are experiencing an problem where we are unable to refresh the data in the footer row of our datatable. We are trying to put a total for the number of rows in the table (or any projection) in the footer for each column and then refresh this each time the data table changes.


The following describes the issue:

1. Using a LazyDataModel for the list in a Session scoped bean
2. Change a filter value on the table

Output: Values of table are reloaded correctly
Output: the footer values are not reloaded/refreshed, but debugging the backing bean shows that the values are correct.

After some debugging we found that the getter for the value in the footer is not being called when a filter is changed.


JSP Code Snippet:

Code: Select all

<p:dataTable id="table_#{id}" var="entry" value="#{eventLogBean.lazyModel}" paginator="true" rows="25" lazy="true"   
      paginatorTemplate="{RowsPerPageDropdown} {FirstPageLink} {PreviousPageLink} {CurrentPageReport} {NextPageLink} {LastPageLink}"  
           rowsPerPageTemplate="5,15,25,35,45,55" resizableColumns="false" scrollable="false" liveScroll="true">  
  
  		<p:column headerText="#{eventLogBean.settings.message_id_columnHeader}" 
			rendered="#{eventLogBean.settings.metadata_id_renderTableColumn}" 
			filterBy="#{entry.id}" 
			sortBy="#{entry.id}">	
			<h:outputText id="ot_id_#{id}" value="#{entry.id}" rendered="#{eventLogBean.settings.metadata_id_renderTableColumn}">
				<f:converter converterId="#{eventLogBean.settings.converter_id_tableConverter}"></f:converter>					
			</h:outputText>
		   	<f:facet name="footer">							
			       <h:outputText id="ot_projection_id_value_#{id}" 
				       rendered="#{eventLogBean.settings.projection_id_defaultProjection_active}" 
				       value="#{eventLogBean.settings.projection_id_defaultProjection_value}">
				        <f:converter converterId="#{eventLogBean.settings.projection_id_defaultProjection_converterId}">
				       </f:converter>						
		   	        </h:outputText>                 				  
         		</f:facet> 		
		</p:column>
Primefaces 3.0M4
Mojarra 2.1.2
Tomcat 6.0

tpacker
Posts: 3
Joined: 21 Oct 2011, 01:18

27 Oct 2011, 21:58

We have coded our own solution to the problem. For anyone who is interested and our own reference, here it is. This isn't the ideal way to solve the problem, but it does work and was the path of least modified code. The ideal solution would be to create a new ajax call to refresh the footer in the table, instead we just piggy backed the refresh on the encoding of the table.

We used the concepts from this post: viewtopic.php?f=3&t=6063&p=33836&hilit= ... ter#p33836 as a guide.

We modified 2 files to achieve this:
org.primefaces.component.datatable.DataTableRenderer
datatable.js

DataTableRenderer Mods:

We found that when a 'regular table' was being created the footer was being created before the table, this was an issue for us because when using a LazyDataModel, the data only loads when the encodeTbody() method is called. To remedy this we modified the encodeRegularTable method and rendered the footer after the body:

Code: Select all

    protected void encodeRegularTable(FacesContext context, DataTable table) throws IOException {
        ResponseWriter writer = context.getResponseWriter();

        writer.startElement("table", null);
        encodeThead(context, table);
        //Mods by Deltamation
        //encodeTFoot(context, table);
        encodeTbody(context, table);
        encodeTFoot(context, table); //Moved to execute after so that the values
                                                  //can be used from the datatable's load() method
        writer.endElement("table");
    }

We found that the footer on the page was not given a generated ID, we needed this id to do the refresh via javascript, so we modified the encodeTFoot method to give the footer an id.

Code: Select all

   protected void encodeTFoot(FacesContext context, DataTable table) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        ColumnGroup group = table.getColumnGroup("footer");
        boolean shouldRender = table.hasFooterColumn() ||
                     (group != null && group.isRendered());

        if(!shouldRender)
            return;

        writer.startElement("tfoot", null);

        //Mods by Deltamation
        String clientId = table.getClientId(context);
        writer.writeAttribute("id", clientId + "_foot_data", null);
        
        if(group != null) {

            for(UIComponent child : group.getChildren()) {
                if(child.isRendered() && child instanceof Row) {
                    Row footerRow = (Row) child;

                    writer.startElement("tr", null);

                    for(UIComponent footerRowChild : footerRow.getChildren()) {
                        if(footerRowChild.isRendered() 
                          && footerRowChild instanceof Column) {
                            encodeColumnFooter(context, table, (Column) footerRowChild);
                        }
                    }

                    writer.endElement("tr");
                }
            }

        }
        else {
            writer.startElement("tr", null);

            for(Column column : table.getColumns()) {
                encodeColumnFooter(context, table, column);
            }

            writer.endElement("tr");
        }
        
        writer.endElement("tfoot");
    }

In a refresh request the table contents are put into the response in the encodeEnd method, we added the encodeTFoot call to this so that whenever the table is refreshed the footer information goes along with it.

Code: Select all

    @Override
	public void encodeEnd(FacesContext context,
                          UIComponent component) throws IOException{
	
        DataTable table = (DataTable) component;

        if(table.isBodyUpdate(context)) {
            encodeTbody(context, table);
            //MOD BY DELTAMATION
            encodeTFoot(context, table);

        } 
        else if(table.isRowExpansionRequest(context)) {
            encodeRowExpansion(context, table);
        }
        else if(table.isRowEditRequest(context)) {
            encodeEditedRow(context, table);
        }
        else if(table.isScrollingRequest(context)) {
            encodeLiveRows(context, table);
        }
        else {
            encodeMarkup(context, table);
            encodeScript(context, table);
        }
        
       
	}
Mods to datatable.js

The mods to the javascript were done to extract the footer that we placed into the response with the datatable.

First we needed the id for the footer, so we created a member in the class to hold it:

Code: Select all

/**
 * PrimeFaces DataTable Widget
 */
PrimeFaces.widget.DataTable = function(id, cfg) {
    this.id = id;
    this.cfg = cfg;
    this.jqId = PrimeFaces.escapeClientId(id);
    this.jq = $(this.jqId);
    this.tbody = this.jqId + '_data';
    
    /** Mods by delta automation*/
    this.tfoot = this.jqId + '_foot_data'

    //Paginator
    if(this.cfg.paginator) {
        this.setupPaginator();
    }
The other modification was to parse out the footer and replace it on the page whenever the table is being modified. This is done in a few methods:

PrimeFaces.widget.DataTable.prototype.paginate

Code: Select all

/**
 * Ajax pagination
 */
PrimeFaces.widget.DataTable.prototype.paginate = function(newState) {
    var options = {
        source: this.id,
        update: this.id,
        process: this.id,
        formId: this.cfg.formId
    };

    var _self = this;
    
    options.onsuccess = function(responseXML) {
        var xmlDoc = $(responseXML.documentElement),
        updates = xmlDoc.find("update");

        for(var i=0; i < updates.length; i++) {
            var update = updates.eq(i),
            id = update.attr('id'),
            content = update.text();

            /**** Mods by Deltamation ****/  
            
            if(id == _self.id){
            	
                //Find the id of the footer
                var contentStr = content.toString();
                var footIndex = contentStr.indexOf('<tfoot');
                
                //Remove the footer information from the body
                if(footIndex > 0){
                	content = contentStr.substring(0,footIndex);
                }
            	
              $(_self.tbody).replaceWith(content);
              
              if(footIndex > 0){
            	  var footdata = contentStr.substring(footIndex);
            	  $(_self.tfoot).replaceWith(footdata);
              }
                
                /** END Mods **/
                
                _self.getPaginator().setState(newState);
                
                if(_self.cfg.resizableColumns) {
                    _self.restoreColumnWidths();
                }
            }
            else {
                PrimeFaces.ajax.AjaxUtils.updateElement.call(this, id, content);
            }
        }
        
        PrimeFaces.ajax.AjaxUtils.handleResponse.call(this, xmlDoc);

        return true;
    };

    var params = {};
    params[this.id + "_paging"] = true;
    params[this.id + "_first"] = newState.recordOffset;
    params[this.id + "_rows"] = newState.rowsPerPage;
    params[this.id + "_page"] = newState.page;
    params[this.id + "_updateBody"] = true;

    options.params = params;
    
    if(this.hasBehavior('page')) {
       var pageBehavior = this.cfg.behaviors['page'];
       
       pageBehavior.call(this, newState, options);
    } else {
       PrimeFaces.ajax.AjaxRequest(options); 
    }
}


and in PrimeFaces.widget.DataTable.prototype.sort

Code: Select all

/**
 * Ajax sort
 */
PrimeFaces.widget.DataTable.prototype.sort = function(columnId, asc) {
    if(this.isSelectionEnabled()) {
        this.clearSelection();
    }
    
    var options = {
        source: this.id,
        update: this.id,
        process: this.id,
        formId: this.cfg.formId
    };

    var _self = this;
    options.onsuccess = function(responseXML) {
        var xmlDoc = $(responseXML.documentElement),
        updates = xmlDoc.find("update");

        for(var i=0; i < updates.length; i++) {
            var update = updates.eq(i),
            id = update.attr('id'),
            content = update.text();

           /**** MODS by Deltamation*******/
  
            
            if(id == _self.id){
            	
                //Find the id of the footer
                var contentStr = content.toString();
                var footIndex = contentStr.indexOf('<tfoot');
                
                //Remove the footer information from the body
                if(footIndex > 0){
                	content = contentStr.substring(0,footIndex);
                }
            	
	              $(_self.tbody).replaceWith(content);
	              
	              if(footIndex > 0){
	            	  var footdata = contentStr.substring(footIndex);
	            	  $(_self.tfoot).replaceWith(footdata);
	              }
	              /**** END MODS ****/
                //reset paginator
                var paginator = _self.getPaginator();
                if(paginator) {
                   paginator.setPage(1, true);
                }
                
                if(_self.cfg.resizableColumns) {
                    _self.restoreColumnWidths();
                }
            }
            else {
                PrimeFaces.ajax.AjaxUtils.updateElement.call(this, id, content);
            }
        }
        
        PrimeFaces.ajax.AjaxUtils.handleResponse.call(this, xmlDoc);

        return true;
    };
    
    var params = {};
    params[this.id + "_sorting"] = true;
    params[this.id + "_sortKey"] = columnId;
    params[this.id + "_sortDir"] = asc;
    params[this.id + "_updateBody"] = true;

    options.params = params;
    
    if(this.hasBehavior('sort')) {
       var sortBehavior = this.cfg.behaviors['sort'];
       
       sortBehavior.call(this, columnId, options);
    } else {
       PrimeFaces.ajax.AjaxRequest(options); 
    }
}
and again in PrimeFaces.widget.DataTable.prototype.filter

Code: Select all

/**
 * Ajax filter
 */
PrimeFaces.widget.DataTable.prototype.filter = function() {
    if(this.isSelectionEnabled()) {
        this.clearSelection();
    }
    
    var options = {
        source: this.id,
        update: this.id,
        process: this.id,
        formId: this.cfg.formId
    };

    var _self = this;
    options.onsuccess = function(responseXML) {
        var xmlDoc = $(responseXML.documentElement),
        updates = xmlDoc.find("update");
        
 
        
        for(var i=0; i < updates.length; i++) {
            var update = updates.eq(i),
            id = update.attr('id'),
            content = update.text();

            /**** MODS BY DELTAMATION*******/
  
            
            if(id == _self.id){
            	
                //Find the id of the footer
                var contentStr = content.toString();
                var footIndex = contentStr.indexOf('<tfoot');
                
                //Remove the footer information from the body
                if(footIndex > 0){
                	content = contentStr.substring(0,footIndex);
                }
            	
              $(_self.tbody).replaceWith(content);
              
              if(footIndex > 0){
            	  var footdata = contentStr.substring(footIndex);
            	  $(_self.tfoot).replaceWith(footdata);
              }
            }
            else {
                PrimeFaces.ajax.AjaxUtils.updateElement.call(this, id, content);

            }
            
        }
        
        PrimeFaces.ajax.AjaxUtils.handleResponse.call(this, xmlDoc);
        
        //update paginator
        var paginator = _self.getPaginator();
        if(paginator) {
            paginator.setPage(1, true);
            paginator.setTotalRecords(this.args.totalRecords, true);
        }

        if(_self.cfg.resizableColumns) {
            _self.restoreColumnWidths();
        }
        
        return true;
    };
    
    var params = {};
    params[this.id + "_filtering"] = true;
    params[this.id + "_updateBody"] = true;

    options.params = params;

    if(this.hasBehavior('filter')) {
       var filterBehavior = this.cfg.behaviors['filter'];
       
       filterBehavior.call(this, {}, options);
    } else {
       PrimeFaces.ajax.AjaxRequest(options); 
    }
}
Primefaces 3.0M4
Mojarra 2.1.2
Tomcat 6.0

camilo.correa
Posts: 11
Joined: 11 Sep 2010, 00:15
Location: Colombia
Contact:

31 Jan 2012, 07:27

@tpacker thank you for post your solution.
Do you already post an issue with this ? I have exactly the same problem and will be great if the solution will be included in the next releases.
I suggest something like:
<p:ajax event="change" update=":component" /> or <p:ajax event="filtering" update=":component" />
Thank you

User avatar
leventgny
Posts: 238
Joined: 24 May 2011, 16:49
Contact:

31 Jan 2012, 08:03

Did you try "filter" event ?
PrimeFaces Team Member

camilo.correa
Posts: 11
Joined: 11 Sep 2010, 00:15
Location: Colombia
Contact:

01 Feb 2012, 01:21

@ironhide thank you very much for your response, I haven't been found the different events that could handle but was my mistake becouse in the official documentation are there (primefaces_users_guide_3M4.pdf page 136).
However I tried with the following code but have the same problem: the component is not updated:

Code: Select all

<p:dataTable 
                        id="datosParametros"
                        var="registro"                
                        value="#{parametrosBB.modelo}"  
                        selectionMode="single"
                        selection="#{parametrosBB.objeto}"
                        rowKey="#{registro.id}"
                        styleClass="tabla_registros_fondo"
                        paginator="true"
                        rows="5"
                        >  
                        <p:ajax event="rowSelect" update=":contenidoPagina:formEntradas" oncomplete="varDialogoParametro.show()" />
                        <p:ajax event="filter" update=":contenidoPagina:formListado:datosParametros:contRegistros" />
                        <f:facet name="header">  
                            PARAMETROS
                        </f:facet>  

                        <p:column headerText="Llave" style="width:200px" sortBy="#{registro.id}" filterBy="#{registro.id}">  
                            #{registro.id}
                        </p:column>  

                        <p:column headerText="Valor" style="width:400px">
                            #{registro.valor} 
                        </p:column>
                        <f:facet name="footer" id="pieTabla">
                            <h:outputText id="contRegistros" value="Total de Parámetros en el sistema: #{parametrosBB.modelo.totalRegistros}"/>                     
                        </f:facet>
                    </p:dataTable> 
To test if the problem is in the ManagedBean I add the following:

Code: Select all

<p:commandButton id="test" value="Test" update=":contenidoPagina:formListado:datosParametros:contRegistros"/> 
When click the button the outputText change the value correctly but with the filtering doesn't update the value.
The parametrosBB.modelo.totalRegistros is assigned in load method.

I appreciate if you have any suggestion

Thank you very much and congratulations for your awesome work!

vineet
Posts: 387
Joined: 14 Oct 2011, 23:40

01 Feb 2012, 05:04

I am looking for something similar. along with the summary row i have chart which needs to be updated along with filter . i couldn't figure out how to use filter event on data table . similar topic is
viewtopic.php?f=3&t=17941 . The only difference is i am not using lazy data model .

tpacker
Posts: 3
Joined: 21 Oct 2011, 01:18

14 Feb 2012, 01:43

An issue has been submitted for this:

http://code.google.com/p/primefaces/iss ... il?id=3496

The current state is closed, Won't Fix.

Perhaps I'm missing something but this seems like a useful feature. If anyone would like this pushed along further please show your support.
Primefaces 3.0M4
Mojarra 2.1.2
Tomcat 6.0

vineet
Posts: 387
Joined: 14 Oct 2011, 23:40

14 Feb 2012, 05:05

I am also facing the same problem and looking for alternatives . Any suggestion on what can be done as showing the summary is a requirement in many of datatables.

PF3.2,Mijarra2.1.4

smithh032772
Posts: 6144
Joined: 10 Sep 2011, 21:10

14 Feb 2012, 06:23

vineet wrote:I am also facing the same problem and looking for alternatives . Any suggestion on what can be done as showing the summary is a requirement in many of datatables.
Any/every value, displayed in a UIcomponent, can be successfully managed by your bean. I have the following, and every time the page is refreshed via AJAX (next/previous buttons i have on the same page), the values are updated in the bean, and the xhtml references the values in the bean.

Code: Select all

<p:dataTable var="ocdSummary"
                value="#{pf_ordersController.getOrderCostDetailsSummaryList(orderCostDetails)}">
    <p:column headerText="">
        <h:outputLabel escape="false" value="#{ocdSummary.label}"/>
    </p:column>
    <p:column headerText="Cost" style="text-align: right !important;">
        <h:outputText escape="false" value="#{ocdSummary.cost}">
            <f:convertNumber minFractionDigits="2" type="currency"/>
        </h:outputText>
    </p:column>
    <p:columnGroup type="footer">
        <p:row>
            <p:column footerText="SUBTOTAL COST for #{orderCostDetails.nbrOfVehicles} vehicle(s) and #{orderCostDetails.nbrOfVehicleOperators} driver(s)" style="text-align: left !important;"/>
            <p:column footerText="#{pf_ordersController.getDecimalFormat('$###,##0.00', orderCostDetails.subtotal)}" style="text-align: right !important;"/>
        </p:row>
    </p:columnGroup>
</p:dataTable>
Rendered as follows:
Image

Now, I will admit that this may not apply to this conversation, since this is a display-only datatable, and there is no filter or any events on this datatable, but my point is that, as I stated earlier, the bean is able to manage the values of the bean attributes referenced by xhtml.

I don't use dataTable 'filter' ajax event; i develop my own 'filter' solution (which consists of p:inputText, filterColumn bean attributes, and filterColumn value passed to SQL/DAO).... a JSF solution.
Howard

PrimeFaces 6.0, Extensions 6.0.0, Push (Atmosphere 2.4.0)
TomEE+ 1.7.4 (Tomcat 7.0.68), MyFaces Core 2.2.9, JDK8
JUEL 2.2.7 | OmniFaces | EclipseLink-JPA/Derby | Chrome

Java EE 6 Tutorial|NetBeans|Google|Stackoverflow|PrimeFaces|Apache

andre.campos
Posts: 13
Joined: 29 Mar 2011, 15:43

31 May 2012, 00:21

This is pretty important to me as well, but I don't like the idea of changing the source code, especially when I'm using the newer versions. I don't want to maintain my own forked version of the primefaces codebase. Having said that, I found a way around. It's actually a hack, but it should keep me away from the primefaces code until Cagatay fixes it.

My problem is actually with the paginator. I have a total for some columns and I'd like that total to be updated when I switch between pages or increase the page size. So here's what I did:

1. Use the footer facet normally:

Code: Select all

              <p:column headerText="#{msg['id']}" sortBy="#{id}"> 
                <h:outputText value="#{item.id}"></h:outputText>  
                <f:facet name="footer"> 
                  <h:outputText value="#{bean.model.totals['id']}" id="totalid"/> 
                </f:facet> 
              </p:column> 
2. Added a hash map in the data model where the key is the column name and the value is the sum of the values in the column (#bean.model.totals); I will skip that implementation.

3. Used p:ajax to fire a client side function that updates the component tree (p:remoteCommand):

Code: Select all

<p:remoteCommand name="updateFilters" update="table:totalid"></p:remoteCommand>
<p:dataTable id="table" value="#{bean.model} ...>
<p:ajax event="page" oncomplete="updateFilters()"/>


Putting it all together:

Code: Select all

<p:remoteCommand name="updateFilters" update="table:totalid"></p:remoteCommand>
<p:dataTable id="tabelaMunicipio" value="#{bean.model} ...>
<p:ajax event="page" oncomplete="updateFilters()"/>
<p:column headerText="#{msg['id']}" sortBy="#{id}"> 
   <h:outputText value="#{item.id}"></h:outputText>  
   <f:facet name="footer"> 
      <h:outputText value="#{bean.model.totals['id']}" id="totalid"/> 
   </f:facet> 
</p:column> 
...
</p:dataTable>


It ain't pretty, but it works. There's a little delay before the footer update, after the new page is loaded, but I can live with that; it's barely noticeable.

Post Reply

Return to “PrimeFaces”

  • Information
  • Who is online

    Users browsing this forum: No registered users and 55 guests