Wednesday, November 17, 2010

Using JavaScript in PeopleSoft: Proxy PeopleSoft Functions

In my last post, I showed you how to do dynamic field level validations in JavaScript with the help of jQuery. In this post we'll take it to the next level by doing validations on submit using a function proxy technique.

JavaScript is a very flexible language. One aspect that separates it from other languages, such as Java, is that everything is an object. This includes functions. At first glance treating a function as an object doesn't seem very useful, but, in fact, it leads to a number of cool capabilities. One of those capabilities is the ability to proxy a function.

Here's a non-PeopleSoft example to illustrate this. Let's say you have a vendor-created web page that already has a validatePage() function that is called by the two buttons, mySaveButton and mySubmitButton.


function validatePage(source) {
    if (!validateRequiredFields())
        return false;
    if (!validateDates())
        return false;
    if (source == 'mySubmitButton') {
        if (!validateTotals())
            return false;
        }
// ...
    return true;
}


We would like to insert another validation step without having to modify the existing code. We can do this by creating a proxy for the original. (This is all standard JavaScript - no jQuery.)


 (function() {
    // proxy validatePage
    var proxied = validatePage; // capture the original function
    validatePage = function() {
        // replace it with our custom version
        var result = true;
        var localArgs = arguments;
        var button = arguments[0];
        if (source == 'mySubmitButton') { result = validateTotals(); }
        if (result) {
                return proxied.apply(this, localArgs); // call the original function.
        } return false;
        };
})();


So what's happening here? The first line of our anonymous function gets a reference to the original validatePage function object. Then we replace the validatePage object with our new function. arguments is a special object available in the body of every function. It's an array of the arguments passed to the function. We use it here to access the argument of the original validatePage function call, in this case the button that called the function. If the calling button was mySubmitButton, my custom validation is run. If the validation passes, the next line calls the proxied function to perform the rest of the validations. There is a lot of flexibility here. We could have executed code after calling the proxied function or not even called it at all. I'll use this later option below when proxying the PeopleSoft saveWarning() function.

How do we apply this to a PeopleSoft page? If you dig around in a typical PeopleSoft page, you'll find that all buttons and links in the main frame that call PeopleCode on the server call the PeopleSoft JavaScript function submitAction_win0(document.win0,'CUST_PB_WRK_CUST_CRSPDSEL_PB'); where the second argument is the id of the button or link field. By proxying this function, we can inject our own validation code that is called whenever a button or link is clicked.



// proxy the peoplesoft submitAction_win0() function
// Called for all peoplecode buttons and links
(function() {
        // proxy submitAction_win0
        var submitProxied = submitAction_win0;
        var submitAction_win0 = function() {
        var button = arguments[1];
        var msg = "";
        switch (button) {
                case "SAVE_BUTTON": // Save
                        if (validateForSave()) {
                                return submitProxied.apply(this, arguments); // call the original function
                        }
                        break;
                case "SUBMIT_BUTTON": // Submit
                        if (validateForSubmit()) {
                                msg = '%BIND(:3)'; // Confirm Submit
                                if (confirm(msg)) {
                                        return submitProxied.apply(this, arguments);
                                }
                        }
                        break;
                default: // all other buttons and links
                        return submitProxied.apply(this, arguments);
                }
        };
})();



Again our proxy function gets a reference to the original function and then substitutes our new function. The switch checks which button called the function and runs a custom validation. If the validation passes, the original function is applied which, in this case, continues the PeopleSoft submit process. The default case simply applies the original function for all other buttons and links.

Proxy the saveWarning Function

To me the standard PeopleSoft save warning is counter-intuitive. I think the OK button should continue the action I originally selected while the Cancel button should cancel that action. Since my pages are exposed to an audience of non-PeopleSoft users, I decided to change it (It's good to be King!). I found that the save warning is raised by, of all things, the saveWarning() function. Here's how I replaced it. Note that I never actually apply the original function, so this proxy completely replaces the it.


var saveWarningURL;
var saveWarningProxied;
var saveWarningTarget;

//proxy the peoplesoft saveWarning(frameName,form,target,url) function
(function() {
saveWarningProxied = saveWarning; // Not doing anything with this
saveWarning = function() {
// console.log("arguments[1]: " + arguments[1]);
localArgs = arguments;
var frameName = arguments[0];
var form = arguments[1];
saveWarningTarget = arguments[2];
saveWarningURL = arguments[3];
var changed=null;

if (form)
 changed = checkFormChanged(form, null);

if (changed==null && top.frames && frameName.length>0 ) {
 objFrame = top.frames[frameName];
 if (objFrame)
   changed=checkFrameChanged(objFrame);
}

if ((changed==null) && top.frames)
checkAnyFrameChanged(top.frames);

if (changed) {
if (confirm('%BIND(:7)')) {
    open(saveWarningURL, saveWarningTarget);
                                }
}
};
})();


Here's what it looks like in action.


That's it for now. In the next post, I plan to show how to create your own modal dialog boxes to replace the standard alert (error/warning) and confirm (yes/no ok/cancel) messages.

Happy coding!



Tuesday, November 2, 2010

Using jQuery in PeopleSoft: Validating, Formatting and Totaling

In my last entry, I introduced using JavaScript and, specifically, the jQuery library to enhance your PeopleSoft pages. Now we'll take a look at how to apply this to browser-side data validation, formatting and totaling. This is the application that inspired me to look into using JavaScript with PeopleSoft in the first place.

Experienced PeopleSoft developers (and why would you be reading this if you're not one?) know that in order to force data validation and page update as soon as a user leaves a field, we must deactivate "deferred processing" so that the page will be submitted when the data changes. On pages with many fields, it can be very disruptive to efficient data entry to submit the page on each change. This also increases the load on the server. On the other hand, real-time data validation and page updates provide the user valuable feedback.

The solution is to do basic data validation, formatting and totaling on the browser-side with JavaScript.
One caveat, never rely solely on browser-side data validation. You cannot control what happens on the user's computer. It is not that hard to hack a web page and subvert the validation. Always do server-side validation also to insure that data you receive is safe and valid.
jQuery makes it easy to detect changes and take action on them. The basic code looks like this.

$(document).ready(function(){
  $('#MY_INPUT').change(function () {
    //do something here
  });
});

The .change() method causes the enclosed function to be called whenever the selected object value is changed and the object loses "focus." An object losses focus when the user "tabs out" or otherwise selects another object on the page. A similar method, .blur(), can also be used. The blur event occurs when the object losses focus, whether or not the value has changed.

Let's say we have a simple time entry page where the user will record her time for the week. Each day has an hours text input element, HOURS1, HOURS2 ... HOURS7. We can watch all seven inputs with one line of code and do some validation and totaling.

$(document).ready(function(){
 $('input:text[id^=HOURS]').change(function () {
  try{
   var hours = parseNumber($(this).val());
   if (hours < 0 || hours > 24){
    alert('Daily hours must be between 0 and 24.');
   }else{
    $(this).val(formatNumber(hours));
   }   
  }catch(er){
   alert ($(this).val() + ' is "' + er.toString() + '"');
  }
 });
 
 // parse text to number.
 function parseNumber(n) {
  var out;
  if (n == ''){
   out = 0;
  } else{
   var out = parseFloat(n);
  }
  if (isNaN(out)) {
   throw 'Not a number';
  }
  return out;
 }

 // format number to text with 2 decimal point. If 0 return ''
 function formatNumber(n) {
  var out;
  if (n == 0) {
   out = '';
  } else {
   out = n.toFixed(2);
  }
  return out;
 }
});


Each time one of the HOURS inputs is changed, this code is called. First we parse the value of the input ($(this).val()) to a number. If it's not a number, the user gets an alert. Same if the value is not between 0 and 24. Finally, the number is formatted with 2 decimal places and we update the input element value.

All this takes place on the browser without submitting the page to the server. Let's take one more step and provide a total. I'll add a totalHours method:

function totalHours() {
  var total = 0;
  $('input:text[id^=HOURS]').each(function () {
   total += parseNumber($(this).val());
  });
  $('#TOTAL_HOURS').val(total.toFixed(2));
 }

and call it from my original code:
...
   }else{
    $(this).val(formatNumber(hours));
    totalHours();
   }   
...

Note how easily I can loop through each of the HOURS inputs and total them up using the each() method.

Now, a couple PeopleSoft-specific tips.

By default, PeopleTools gives each input an id attribute equal to the concatenated record and field names. This can be awkward and sometimes not very descriptive. You can override this behavior by setting the "Page Field Name" in the "General" tab of the Edit Box Page Field properties. The value you set here will be used as the id and name of the element in the html. Just be sure the value you set is unique for the page.

The Edit Box "Display-Only Appearance" should be set to "Disabled Edit Control" in the "Use" tab of Edit Box Page Field properties. This causes the Edit Box to become an "addressable" text <input> element rather than <span> element with no id. This does cause some formatting issues depending on the browser. I'll describe my solution to this in a later post.

So now you can validate, format and total data on the browser side without having to submit your page to the server.

In my next post, I'll show you how to intercept the delivered PeopleSoft submit process so you can do validations and verifications on submit.