Wednesday, April 13, 2011

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

This is the continuation of the part I where I intended to add client-side callbacks for any PrimeFaces Ajax calls. PrimeFaces components normally have callbacks like onstart and oncomplete, but not for all events. PrimeFaces tree has e.g. onselectStart and onselectComplete, but what is about onexpandStart and onexpandComplete in dynamic trees? I guess, all such callbacks would blow up the component interface. I believe all existing component libraries mimic each other and have countless callbacks and listeners as component attributes. Why do they need them at all for Ajax calls? There is f:ajax and derivations like p:ajax which already have "listener", "execute", "render" attributes and provide all Ajax callbacks with "onevent" attribute automatically. Do they really need to blow up component attributes or would be a simple attaching of Ajax behaviors enough? Well. It's not the topic of this post.

The component blockUI is used here as reference implementation. It prevents user activity with any part of the page as long as an Ajax request / response cycle is running. We need a component class BlockUI.java, a renderer class BlockUIRenderer.java and an JavaScript blockUI.js. The component class is trivial. It has attributes "widgetVar", "for", "forElement", "source" und getter / setter. More interesting are the renderer and the script.

BlockUIRenderer.java

public class BlockUIRenderer extends CoreRenderer
{
 private static final Log LOG = LogFactory.getLog(BlockUIRenderer.class);

 @Override
 public void encodeEnd(FacesContext fc, UIComponent component) throws IOException {
  encodeMarkup(fc, component);
  encodeScript(fc, component);
 }

 public void encodeMarkup(FacesContext fc, UIComponent component) throws IOException {
  ResponseWriter writer = fc.getResponseWriter();

  writer.startElement("div", null);
  writer.writeAttribute("id", component.getClientId(fc) + "_content", null);
  writer.writeAttribute("style", "display: none;", null);
  renderChildren(fc, component);
  writer.endElement("div");
 }

 protected void encodeScript(FacesContext fc, UIComponent component) throws IOException {
  ResponseWriter writer = fc.getResponseWriter();
  BlockUI blockUI = (BlockUI) component;
  String clientId = blockUI.getClientId(fc);
  String widgetVar = blockUI.resolveWidgetVar();

  String source;
  UIComponent sourceComponent = blockUI.findComponent(blockUI.getSource());
  if (sourceComponent == null) {
   source = "";
   LOG.warn("Source component could not be found for 'source' = " + blockUI.getSource());
  } else {
   source = sourceComponent.getClientId(fc);
  }

  String target;
  if (blockUI.getFor() != null) {
   UIComponent targetComponent = blockUI.findComponent(blockUI.getFor());
   if (targetComponent == null) {
    target = "";
    LOG.warn("Target component could not be found for 'for' = " + blockUI.getFor());
   } else {
    target = targetComponent.getClientId(fc);
   }
  } else if (blockUI.getForElement() != null) {
   target = blockUI.getForElement();
  } else {
   target = "";
   LOG.warn("Target is missing, either 'for' or 'forElement' attribute is required");
  }

  UIComponent facet = blockUI.getFacet("events");

  // collect all f:param
  List<UIParameter> uiParams = new ArrayList<UIParameter>();
  if (facet instanceof UIParameter) {
   // f:facet has one child and that's f:param
   uiParams.add((UIParameter) facet);
  } else if (facet != null && facet.getChildren() != null) {
   // f:facet has no or more than one child (all children included into an UIPanel)
   for (UIComponent kid : facet.getChildren()) {
    if (kid instanceof UIParameter) {
     uiParams.add((UIParameter) kid);
    }
   }
  }

  // build a regular expression for event key, value pair.
  String eventRegExp;
  if (uiParams.isEmpty()) {
   // no or empty "events" facet means all events of the given source are accepted
   eventRegExp = "/" + Constants.PARTIAL_SOURCE_PARAM + "=" + source + "(.)*$/";
  } else {
   StringBuffer sb = new StringBuffer("/");
   sb.append(source);
   sb.append("_");

   Iterator<UIParameter> iter = uiParams.iterator();
   while (iter.hasNext()) {
    UIParameter param = iter.next();

    // not set event name / value means any name / value is accepted
    sb.append("(");
    sb.append(param.getName() != null ? param.getName() : "(.)*");
    sb.append("=");
    sb.append(param.getValue() != null ? param.getValue() : "(.)*");
    sb.append("$)");

    if (iter.hasNext()) {
     sb.append("|");
    }
   }

   sb.append("/");
   eventRegExp = sb.toString();
  }

  writer.startElement("script", blockUI);
  writer.writeAttribute("type", "text/javascript", null);
  writer.write(widgetVar + " = new JsfToolkit.widget.BlockUI('"
               + ComponentUtils.escapeJQueryId(clientId) + "','"
               + ComponentUtils.escapeJQueryId(source) + "','"
               + ComponentUtils.escapeJQueryId(target) + "'," + eventRegExp + ");");
  writer.write(widgetVar + ".setupAjaxSend();");
  writer.write(widgetVar + ".setupAjaxComplete();");

  writer.endElement("script");
 }

 @Override
 public boolean getRendersChildren() {
  return true;
 }

 @Override
 public void encodeChildren(FacesContext fc, UIComponent component) throws IOException {
  // nothing to do
 }
}
The most important thing is the building of regular expressions. If we are not interesting in any special events, we will only check whether the source component in available in passed Ajax parameters (see the JS script below). The regular expression using for this check is
 
/javax.faces.source=<source>(.)*$/
 
where <source> is the clientId of the source component. Radio buttons and checkboxes I mentioned in the part I send e.g.
 
javax.faces.source=accessLevels_1
javax.faces.source=accessLevels_2
javax.faces.source=accessRights_1
javax.faces.source=accessRights_2
 
The regular expressions should be therefore
 
/javax.faces.source=accessLevels(.)*$/
/javax.faces.source=accessRights(.)*$/
 
If we interesting in specified events, we build a regular expression for each event and concatenate them with the logical OR-operator. A regular expression for each event looks as follows
 
/<source>_(<event name>=<event value>)$/
 
If the event name or value is not important, we use (.)* for any strings (empty is also allowed). Some examples:
 
/dtPropTemplates_(filtering=true)$|dtPropTemplates_(sorting=true)$/
/treeSocs_(action=SELECT)$/
/selectedGroupsUsers_(instantSelectedRowIndex=(.)*)$/
 
Corresponding Ajax requests have parameters (Ajax options):
 
dtPropTemplates_filtering=true
dtPropTemplates_sorting=true
treeSocs_action=SELECT
selectedGroupsUsers_instantSelectedRowIndex=<rowId>
// <rowId> is the internal Id of the currently selected row
 

blockUI.js

The script provides two functions to register ajaxSend and ajaxComplete events where I check Ajax options in order to find the appropriate event. The check uses mentioned above regular expressions.
JsfToolkit.widget.BlockUI = function(id, source, target, regExp) {
 var clientId = id;
 var sourceId = source;
 var targetId = target;
 var eventRegExp = regExp;
 
 // global settings
 jQuery.blockUI.defaults.theme = true;
 jQuery.blockUI.defaults.fadeIn = 0;
 jQuery.blockUI.defaults.fadeOut = 0;
 jQuery.blockUI.defaults.applyPlatformOpacityRules = false;
 
 /* public access */
 
 this.setupAjaxSend = function () {
  jQuery(sourceId).ajaxSend(function(event, xhr, ajaxOptions) {
   // first, check if event should be handled 
   if (isAppropriateEvent(ajaxOptions)) {
    var targetEl = jQuery(targetId);

    // second, check if the target element has been found
    if (targetEl.length > 0) {
     // block the target element
     targetEl.block({message: jQuery(clientId + "_content").html()});

     // get the current counter
     var blocksCount = targetEl.data("blockUI.blocksCount");
     if (typeof blocksCount === 'undefined') {
      blocksCount = 0;
     }

     // increase the counter
     targetEl.data("blockUI.blocksCount", blocksCount+1);
    }
   }
  });
 }
 
 this.setupAjaxComplete = function () {
  jQuery(sourceId).ajaxComplete(function(event, xhr, ajaxOptions) {
   // first, check if event should be handled
   if (isAppropriateEvent(ajaxOptions)) {
    var targetEl = jQuery(targetId);

    // second, check if the target element has been found
    if (targetEl.length > 0) {
     // get the current counter
           var blocksCount = targetEl.data("blockUI.blocksCount");

           // check the counter
     if (typeof blocksCount !== 'undefined') {
      if (blocksCount == 1) {
       // unblock the target element and reset the counter
       jQuery(targetId).unblock();
       targetEl.data("blockUI.blocksCount", 0);
      } else if (blocksCount > 1) {
       // only decrease the counter
       targetEl.data("blockUI.blocksCount", blocksCount-1);
      }
     }
    }
   }
  });
 }
 
 /* private access */
 
 var isAppropriateEvent = function (ajaxOptions) {
  if (typeof ajaxOptions === 'undefined' || ajaxOptions == null ||
   typeof ajaxOptions.data === 'undefined' || ajaxOptions.data == null) {
   return false;
  }

  // split options around ampersands
  var params = ajaxOptions.data.split(/&/g);
  
  // loop over the ajax options and try to match events
  for (var i = 0; i < params.length; i++) {
   if (eventRegExp.test(params[i])) {
    return true;
   }
  }

  return false;
 }
}
ajaxSend increase a counter bound to target element and ajaxComplete decrease the counter. This is necessary if many sources blocks the same target. The target is unblocked if the counter is 1. The parsing of Ajax options is the main part and the clou of the question how to find any Ajax events to be able to fire client-side callbacks.

6 comments:

  1. well, very nice!
    could you post the source code blockUI.java?

    ReplyDelete
  2. There just setter / getter. Here the code http://paste.kde.org/49351/ You need yet jquery.blockUI.js from the blockUI homepage.

    ReplyDelete
  3. Hello, link is broken, can you give an actual one?
    Thanks!

    ReplyDelete
  4. Hello, it don't work for me, i use the PrimefacesM2-SNAP, could you post the complete source-package?

    ReplyDelete
  5. PrimeFaces M2 uses other events at some places (I mentioned this problem in the first part of this post). You should look into JS files and figure out their names.

    ReplyDelete

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