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.