Thursday, July 21, 2011

Unified JSF Connection (Ajax) Status Indicator

Real JSF web applications consist of various ajaxifed components. The good practise is to tell users that they should be waiting for something to finish. A global Ajax Status Indicator seems to be very handy in ajaxifed applications. Such components exist e.g. in RichFaces, ICEFaces and PrimeFaces. If you only use one of these component libraries, you can skip this post :-) But what is about if you use a mix composed of standard f:ajax with any standard and not standard components + ajaxified components from any libraries? I have counted f:ajax and PrimeFaces' p:ajax appearances in my applications. Approximately 50% to 50%. Probably you used f:ajax too because p:ajax had issues in early PrimeFaces versions. The question is now how to adjust p:ajaxStatus in order to consider standard Ajax requests in addition to PrimeFaces Ajax requests based on jQuery Ajax? We can add global callbacks for event handling from standard JSF
 
jsf.ajax.addOnEvent(callback)
jsf.ajax.addOnError(callback)
 
Let me extend PrimeFaces AjaxStatus, register new global callbacks and show an example in action. At first we have to add the standard JS library for Ajax and our extended script ajaxstatus.js.
import javax.faces.application.ResourceDependencies;
import javax.faces.application.ResourceDependency;

@ResourceDependencies({
    @ResourceDependency(library="primefaces", name="jquery/jquery.js"),
    @ResourceDependency(library="primefaces", name="core/core.js"),
    @ResourceDependency(library="primefaces", name="ajaxstatus/ajaxstatus.js"),
    @ResourceDependency(library="javax.faces", name="jsf.js"),
    @ResourceDependency(library="js", name="ajaxstatus.js")
})
public class AjaxStatus extends org.primefaces.component.ajaxstatus.AjaxStatus
{
}
Renderer class is simple
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import java.io.IOException;

public class AjaxStatusRenderer extends org.primefaces.component.ajaxstatus.AjaxStatusRenderer
{
    protected void encodeScript(FacesContext context, org.primefaces.component.ajaxstatus.AjaxStatus st)
        throws IOException {
        AjaxStatus status = (AjaxStatus)st;
        ResponseWriter writer = context.getResponseWriter();
        String clientId = status.getClientId(context);
        String widgetVar = status.resolveWidgetVar();

        writer.startElement("script", null);
        writer.writeAttribute("type", "text/javascript", null);

        writer.write(widgetVar + " = new PrimeFaces.widget.ExtendedAjaxStatus('" + clientId + "');");

        encodeCallback(context, status, widgetVar, "ajaxSend", "onprestart", AjaxStatus.PRESTART_FACET);
        encodeCallback(context, status, widgetVar, "ajaxStart", "onstart", AjaxStatus.START_FACET);
        encodeCallback(context, status, widgetVar, "ajaxError", "onerror", AjaxStatus.ERROR_FACET);
        encodeCallback(context, status, widgetVar, "ajaxSuccess", "onsuccess", AjaxStatus.SUCCESS_FACET);
        encodeCallback(context, status, widgetVar, "ajaxComplete", "oncomplete", AjaxStatus.COMPLETE_FACET);

        writer.endElement("script");
    }
}
Note: Prestart event "ajaxSend" is not supported in the standard Ajax, but it's rarely used anyway. "ajaxStart" is normally enough. JavaScript ajaxstatus.js is a little bit complicate. I register there global handlers for Ajax events by jsf.ajax.addOnError / jsf.ajax.addOnEvent.
PrimeFaces.widget.ExtendedAjaxStatus = function(id) {
    this.id = id;
    this.jqId = PrimeFaces.escapeClientId(this.id);
}

PrimeFaces.widget.ExtendedAjaxStatus.prototype.bindFacet = function(eventName, facetToShow) {
    var _self = this;

    // jQuery
    jQuery(document).bind(eventName, function() {
        _self.showFacet(facetToShow);
    });

    // Standard
    if (eventName == "ajaxError") {
        jsf.ajax.addOnError(this.processAjaxOnErrorFacet(facetToShow));
    } else {
        jsf.ajax.addOnEvent(this.processAjaxOnEventFacet(eventName, facetToShow));
    }    
}

PrimeFaces.widget.ExtendedAjaxStatus.prototype.bindCallback = function(eventName, fn) {
    // jQuery
    jQuery(document).bind(eventName, fn);

    // Standard
    if (eventName == "ajaxError") {
        jsf.ajax.addOnError(this.processAjaxOnErrorCallback(fn));
    } else {
        jsf.ajax.addOnEvent(this.processAjaxOnEventCallback(eventName, fn));
    }
}

PrimeFaces.widget.ExtendedAjaxStatus.prototype.processAjaxOnEventFacet = function(eventName, facetToShow) {
    var _self = this;

    function processEvent(data) {
        if (eventName == "ajaxStart" && data.status == "begin") {
            _self.showFacet(facetToShow);
        } else if (eventName == "ajaxComplete" && data.status == "complete") {
            _self.showFacet(facetToShow);
        } else if (eventName == "ajaxSuccess" && data.status == "success") {
            _self.showFacet(facetToShow);
        }
    }

    return processEvent;
}

PrimeFaces.widget.ExtendedAjaxStatus.prototype.processAjaxOnErrorFacet = function(facetToShow) {
    var _self = this;

    function processEvent() {
        _self.showFacet(facetToShow);
    }

    return processEvent;
}

PrimeFaces.widget.ExtendedAjaxStatus.prototype.processAjaxOnEventCallback = function(eventName, fn) {
    function processEvent(data) {
        if (eventName == "ajaxStart" && data.status == "begin") {
            fn();
        } else if (eventName == "ajaxComplete" && data.status == "complete") {
            fn();
        } else if (eventName == "ajaxSuccess" && data.status == "success") {
            fn();
        }
    }

    return processEvent;
}

PrimeFaces.widget.ExtendedAjaxStatus.prototype.processAjaxOnErrorCallback = function(fn) {
    function processEvent() {
        fn();
    }

    return processEvent;
}

PrimeFaces.widget.ExtendedAjaxStatus.prototype.showFacet = function(facetToShow) {
    jQuery(this.jqId).children().hide();
    jQuery(this.jqId + '_' + facetToShow).show();
}
Using on the page along with JS handling could be
<p:ajaxStatus onstart="ajaxOnStartIndicator()" onerror="ajaxOnErrorIndicator()" onsuccess="ajaxOnSuccessIndicator()"/>
<h:graphicImage id="ajaxIndicatorActive" library="img" name="connect_active.gif"
    style="display: none;" styleClass="ajaxIndicator"/>
<h:graphicImage id="ajaxIndicatorCaution" library="img" name="connect_caution.gif"
    style="display: none;" styleClass="ajaxIndicator" title="Connection problem" alt="Connection problem"/>
function ajaxOnStartIndicator() {
    document.body.style.cursor = 'wait';
    jQuery("#ajaxIndicatorCaution").css("display", "none");
    jQuery("#ajaxIndicatorActive").css("display", "block");

    // get the current counter
    var jqDoc = jQuery(document);
    var requestCount = jqDoc.data("ajaxStatus.requestCount");
    if (typeof requestCount === 'undefined') {
        requestCount = 0;
    }

    // increase the counter
    jqDoc.data("ajaxStatus.requestCount", requestCount+1);
}

function ajaxOnSuccessIndicator() {
    // get the current counter
    var jqDoc = jQuery(document);
    var requestCount = jqDoc.data("ajaxStatus.requestCount");

    // check the counter
    if (typeof requestCount !== 'undefined') {
        if (requestCount == 1) {
            // hide indicators
            document.body.style.cursor = 'auto';
            jQuery("#ajaxIndicatorActive").css("display", "none");
            jQuery("#ajaxIndicatorCaution").css("display", "none");
            jqDoc.data("ajaxStatus.requestCount", 0);
        } else if (requestCount > 1) {
            // only decrease the counter
            jqDoc.data("ajaxStatus.requestCount", requestCount-1);
        }
    }
}

function ajaxOnErrorIndicator() {
    document.body.style.cursor = 'auto';
    jQuery("#ajaxIndicatorActive").css("display", "none");
    jQuery("#ajaxIndicatorCaution").css("display", "block");
    // reset counter
    jQuery(document).data("ajaxStatus.requestCount", 0);
}
The implementation is smart enough and avoid collisions with parallel running Ajax requests (s. counters). Furthermore, the cursor is set to 'wait' if an Ajax request is running. It's how desktop applications behaves.

I took connection icons from ICEFaces showcase.

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hi Oleg. I am trying to recreate this blog entry. However, I am a bit confused on this JSF code of yours:


    [h:graphicImage id="ajaxIndicatorActive" library="img" name="connect_active.gif"
    style="display: none;" styleClass="ajaxIndicator"/]

    As far as I know h:graphicImage does not have attribute "library" and "name". Where are these come from, Oleg?

    (How do I post code in the comment box)

    ReplyDelete
    Replies
    1. Sure, it has these attributes in JSF 2. http://javaserverfaces.java.net/nonav/docs/2.1/vdldocs/facelets/h/graphicImage.html

      Delete

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