Alternative to the Average AJAX ActionStatus

Monday, July 18, 2011

Visualforce offers some great shortcuts that makes for faster development and a more consistent user experience. It is these tools to which Salesforce refers when they proudly advertise the short development cycles that programmers experience when working on the Force.com platform. It makes my life as a developer easier, and I really appreciate that. Sometimes, however, you want to add a slightly more advanced feature, one that requires pushing your code off of and beyond the tracks that the platform provides you.

One area that Visualforce has made very simple is Ajax. Adding just one or two Visualforce tags and attributes can produce a responsive page that uses Ajax for updating information. One such tag is the actionStatus tag. Ajax operation will work just fine if you don't use this tag, but the user won't know that an Ajax update is, in fact, taking place. Add this tag to the page and reference it from the same place that called the Ajax to tell the user, "Hey, wait a second. We've got some data coming from the server, so you should wait for it to arrive before continuing." Here's the simplest way to use this tag, taken from the SF documentation for commandButton:

<apex:page controller="exampleCon">
  <apex:form id="theForm">
    <apex:outputText value="{!varOnController}"/>
    <apex:commandButton action="{!update}" rerender="theForm" value="Update" id="theButton"/>
  </apex:form>
</apex:page>

Code Review: In this example, the secret sauce that adds the Ajax is the action and rerender attributes on the commandButton tag. Clicking on the button will send an Ajax message back to the server that will get the latest value of varOnController, which is a property of the controller object that we assume is changing with time. When the message returns to the page, the element that the outputText element creates will be replaced with the new value.

Now, let's imagine that it takes a few seconds for the controller to get this updated value back to us. The user will sit there, staring at the page, wondering if the button-click didn't work. We should give him some feedback:

<apex:page controller="exampleCon">
  <apex:form id="theForm">
    <apex:outputText value="{!varOnController}"/>
    <apex:actionStatus startText=" (working...)" stopText=" (done)" id="updateStatus"/>
    <apex:commandButton action="{!update}" rerender="theForm" status="updateStatus" value="Update" id="theButton"/>
  </apex:form>
</apex:page>

Code Review: This code should be the same as the first one, but this time we've added an actionStatus tag, which we attach to the commandButton's Ajax call by using the status attribute on commandButton. With this simple change, when the Ajax call begins, ( working...) will appear next to the outputText, and when it finishes, it will change to (done). This is a pretty good quick-and-done solution for user feedback.

The Problem: But what if we had a page that was heavy on form inputs, one that requires lots of user interaction? Is there a way to tell the user to wait before filling in more fields until the page updates? Well, I've found two possible solutions. Neither is perfect, but they both get the job done. What we want to achieve is to disable all inputs and buttons on the form while the page update is taking place.

1) The first way to accomplish this is to create a drop-down curtain effect, which drops a see-through curtain to capture clicks over the form. Here's how solution number 1 works:

<apex:pageBlockSection title="Form 1" id="formSection" collapsible="false">
  <div id="loadingCurtain">
  <apex:inputField value="{!myObject.Name}"/>
  <apex:inputField value="{!myObject.Address}"/>
  <apex:inputField value="{!myObject.OtherFields}"/>
  <apex:commandButton action="{!update}" rerender="formSection" onclick="showLoadingDiv();" oncomplete="hideLoadingDiv();" value="Update" id="theButton"/>
</apex:pageBlockSection>

Code Review: This is just a pageBlockSection that is contained inside form tags, which aren't shown. This one has three fields, though it could have more, and a button to post it back to the server. Instead of using the actionStatus tag, we're going to call our own Javascript functions to manipulate the loading curtain:

$j = jQuery.noConflict();

//This escapes SF-created IDs
function esc(myid) {
  return '#' + myid.replace(/(:|\.)/g,'\\\\$1');
}

function showLoadingDiv() {
  var divToScreenEsc = esc("{!$Component.hardCostSection}");
  var newHeight = $j(divToScreenEsc + " .pbSubsection").css("height");//Just shade the body, not the header
  $j("#loadingDiv").css("background-color", "black").css("opacity", 0.35).css("height", newHeight).css("width", "80%");
}
function hideLoadingDiv() {
  $j("#loadingDiv").css("background-color", "black").css("opacity", "1").css("height", "0px").css("width", "80%");
}

I'm using jQuery here. I've had bad experiences trying to use vanilla Javascript, and I've vowed to never use it again. jQuery gives consistent results across browser DOMs and across Javascript implementations itself. Note the esc function. This is necessary to escape the non-standard character in SF-created IDs for use in jQuery selectors, and was a solution created by Wes Nolte. See his blog post on the solution here. Beyond that, we're just selecting the loadingDiv and setting some styles, pretty simple.


2) The second solution is to avoid adding another element to the DOM, and just using Javascript to disable all selectable fields and buttons in the form:

<apex:pageBlockSection title="Form 1" id="formSection" collapsible="false">
  <apex:inputField value="{!myObject.Name}"/>
  <apex:inputField value="{!myObject.Address}"/>
  <apex:inputField value="{!myObject.OtherFields}"/>
  <apex:commandButton action="{!update}" rerender="formSection" onclick="showLoadingDiv2();" oncomplete="hideLoadingDiv2();" value="Update" id="theButton"/>
</apex:pageBlockSection>

Code Review: This is the same VF snippet as above, minus the curtain div. This time, we'll use Javascript to set the styles of the form elements themselves.

function showLoadingDiv2() {
  var divToScreenEsc = esc("{!$Component.formSection}");
  $j(divToScreenEsc).css("opacity", "0.35");
  $j(divToScreenEsc + " input, " + divToScreenEsc + " select").attr("disabled", "true");
}
function hideLoadingDiv2() {
  var divToScreenEsc = esc("{!$Component.formSection}");
  $j(divToScreenEsc).css("opacity", "1");
  $j(divToScreenEsc + " input, " + divToScreenEsc + " select").attr("disabled", "false");
}

This will disable all input and select elements in the specified form section as well as turn the entire section slightly transparent by setting the opacity to 35%.

That's it, really. Two simple solutions that I came up with to give a better user experience. Improve up it! I'm not a pro at HTML and Javascript, and maybe you can help make it better - leave a comment!

Before concluding this post, I want to mention the more elegant version of this that you should investigate:
Keep in mind that the actionStatus tag has both onstart and onstop attributes, from which you can call the necessary Javascript functions from there. The advantage to doing it this way is that the functionality is again tied to the actionStatus tag . This adds a layer of abstraction between the functionality and its visual status indicator. You would call the Javascript functions from the actionStatus tag instead of the Ajax initiating tags, such as a commandButton, keeping your code DRY and happy.

No comments:

Post a Comment