Saturday, April 9, 2011

Hooking of PrimeFaces Ajax calls as a kind of callbacks. Part I.

Using PrimeFaces I have needed client-side callbacks for events causing Ajax calls like sorting, pagination, filtering of datatable or tree node expansion. I can understand that it's impossible to define all kinds of callbacks. Therefore I was looking for a generic way and believe I have found it. I'm able now to "add" any callbacks to any components. Firstly some words about PrimeFaces Ajax driven events. A PrimeFaces Ajax request triggered by any component has a parameter describing the event. It consist of component's clientId, underline, name and value, e.g. myTabview_tabChange=true. We are especially interesting in names and values. They are e.g.
 
Datatable:
name="filtering" value="true"
name="sorting" value="true"
name="paging" value="true"

Tree:
name="action" value="SELECT"
name="action" value="EXPAND"

TabView:
name="tabChange" value="true"
 
This anatomy is not disclosed. Name / value pairs are undocumented, so that you need to look into JS files (for something like params[this.id + '_tabChange'] = true;) or corresponding component renderers to figure out what are the parameters of any event. The other problem is that a name / value pair can be changed in next releases. The new tree (since PrimeFaces 3.x) will have e.g.
 
name="instantSelection" value="<Node Id>"
name="expandNode" value="<Node Id>"
 
It would be very nice and appreciated by community if the PrimeFaces team could standardize and document all Ajax driven events. Please :-)

jQuery has a capability to register callbacks for Ajax events. There are ajaxStart .... ajaxComplete events in jQuery. With ajaxStart you can register a handler to be called when the first Ajax request begins, with ajaxComplete a handler to be called when Ajax requests complete, etc. We can use them to catch PrimeFaces Ajax calls and check passed parameters in order to figure out whether the event occurs we are interesting in.

I'm going to demonstrate this approach on basis of a component I have implemented recently. This component calls blockUI and it's based on the jQuery plugin blockUI. By means of this component we can block any piece of page during various Ajax calls. Very handy if you have e.g. a large datatable and sorting, filtering, pagination takes much time. The component has the following parameters: "widgetVar" for the underlying widget (as all PrimeFaces components have it), "for", "forElement" to specify target component or HTML element which gets blocked and "source" to specify source component which causes an Ajax request. Events, we are interesting in, are specified via a facet "events" and f:param within the facet. I'm going to start with facelets part and show three examples.

Playing together with p:dataTable
 
<h:panelGroup id="dtPropTemplatesGroup" layout="block">
 <p:dataTable id="dtPropTemplates" var="propTemplate"
       value="#{propTemplates.templates}" paginator="true" rows="20" ...>
  ...
 </p:dataTable>
 <jtcomp:blockUI source="dtPropTemplates" for="dtPropTemplates">
  <f:facet name="events">
   <f:param name="filtering" value="true"/>
   <f:param name="paging" value="true"/>
   <f:param name="sorting" value="true"/>
  </f:facet>
  <h:panelGrid columns="2">
   <h:graphicImage library="themes" name="app/images/ajaxIndicator.gif"/>
   <h:outputText value="Please wait, data are processing ..."/>
  </h:panelGrid>
 </jtcomp:blockUI>
</h:panelGroup>
 
Filtering, paging and sorting events display a half transparent overlay over the same table and a rotating Ajax indicator with the message "Please wait, data are processing ...".


Playing together with p:tree
 
<h:panelGroup id="treeSocsGroup" layout="block">
 <p:tree id="treeSocs" value="#{socsBean.socsRootNode}" var="socNode" 
  dynamic="true" cache="true" selectionMode="single" 
  nodeSelectListener="#{socsBean.onNodeSelect}"
  nodeExpandListener="#{socsBean.onNodeExpand}" ...>
  <p:treeNode>
   <h:outputText value="#{socNode.displayName}" escape="false"/>
  </p:treeNode>
  <p:treeNode type="leafSoc" styleClass="leafSoc">
   <h:outputText value="#{socNode.displayName}" styleClass="leafContent"/>
  </p:treeNode>
 </p:tree>
 <jtcomp:blockUI source="treeSocs" forElement="overviewSocsRightGroup">
  <f:facet name="events">
   <f:param name="action" value="SELECT"/>
  </f:facet>
  // the same stuff as above
 </jtcomp:blockUI>

 <h:panelGroup id="overviewSocsRightGroup" layout="block">
  // menus, tables and other stuff
 </h:panelGroup>
</h:panelGroup>
 
A click on any tree node blocks the panelGroup with id "overviewSocsRightGroup" and the mentioned above overlay, indicator and message are displayed.


Playing together with p:ajax

Sometimes we are not interesting in any special events. In this case no f:facet for events is required. Assume we have a table, radio buttons and checkboxes. A click on any table row should block the radio buttons and checkboxes. A click on any radio button or checkbox should block the table, radio buttons and checkboxes. Remaining part of the page should stay untouched. Note: tables should be wrapped with a div element if they have to be blocked because a table can't be blocked nice in all browsers.
 
<h:panelGroup id="rightsGroup" layout="block">
 <h:panelGroup id="selectedGroupsUsersGroup">
  <p:dataTable id="selectedGroupsUsers" var="selGrUs" value="#{rightsBean.selectedGroupsUsers}" dynamic="false"
        selection="#{rightsBean.selectedGroupUser}" selectionMode="single" update="levelsRightsGrid">
   ...
  </p:dataTable>
  <jtcomp:blockUI source="selectedGroupsUsers" forElement="levelsRightsPanel">
   <f:facet name="events">
    <f:param name="instantSelectedRowIndex"/>
   </f:facet>
   // the same stuff as above
  </jtcomp:blockUI>
 </h:panelGroup>

 <h:panelGrid id="levelsRightsGrid" columns="2">
  <h:panelGroup id="accessLevelsGroup">
   <h:selectOneRadio id="accessLevels" value="#{rightsBean.level}" layout="pageDirection">
    <f:selectItem itemLabel="Custom" itemValue="Custom" itemDisabled="true"/>
    <f:selectItems value="#{rightsBean.selectableLevels}"/>
    <p:ajax event="click" process="@this" update="accessRightsGroup,selectedGroupsUsersGroup"/>
   </h:selectOneRadio>
   <jtcomp:blockUI source="accessLevels" for="rightsGroup">
    // the same stuff as above
   </jtcomp:blockUI>
  </h:panelGroup>
  
  <h:panelGroup id="accessRightsGroup">
   <h:selectManyCheckbox id="accessRights" value="#{rightsBean.selectedAccessRights}" layout="pageDirection">
    <f:selectItems value="#{rightsBean.accessRights}"/>
    <p:ajax event="click" process="@this" update="accessLevelsGroup,selectedGroupsUsersGroup"/>
   </h:selectManyCheckbox>
   <jtcomp:blockUI source="accessRights" for="rightsGroup">
    // the same stuff as above
   </jtcomp:blockUI>
  </h:panelGroup>
 </h:panelGrid>
</h:panelGroup>
 

The event instantSelectedRowIndex doesn't have a specified value. That means we are not interesting in a value (everything is valid). Unfortunately Ajax handlers can not be bound with jQuery .live() method. Therefore I had to wrap radio button and checkboxes with a h:panelGroup to get blockUI updated every time together with corresponding radio button and checkboxes.

The second part will cover the implementation and reveal more details. Stay tuned.

3 comments:

  1. Great post!!!

    i was using blockUI in composite component but your solution is more complete, something like this: http://pastebin.com/fxXqkvqY

    thanks for the sharing your solution.

    ReplyDelete
  2. i like this cc solution since i dont need to block the ui in such complexes cases like in Oleg solution so in simple cases the composite one can help, here is a little bit 'generic' cc, i hope it helps someone.


    http://pastebin.com/yQrufpJr

    ReplyDelete
  3. thank's
    can you share the bean for the example of tree.
    thank's

    ReplyDelete

Note: Only a member of this blog may post a comment.