Thursday, August 03, 2017

Event Mapping: Fluid Landing Page "Dot" Buttons

The bottom of a Fluid landing page contains dots that identify how many landing pages the user can view as well as the position of the current landing page within the user's landing page collection. This piece of information is quite useful on a swipe-enabled mobile device, but somewhat meaningless on my mac. A few months ago I was helping a PeopleSoft customer with their Fluid implementation. This rather brilliant customer made those dots clickable to navigate between landing pages. To implement this behavior, the customer modified delivered PeopleTools JavaScript and CSS definitions, which, unfortunately, presents a bit of an upgrade problem. In a recent webinar I mentioned that one of the things that excites me about Fluid is that everything is a component. What that means is we can use Event Mapping anywhere. Landing Pages are no exception. With that in mind, I wondered what this solution would look like with Event Mapping.

Those little dots at the bottom of the page are created through JavaScript. To enhance them, therefore, we will need to write some JavaScript. Because the original implementation of this behavior modified delivered PeopleTools JavaScript, the author was able to leverage existing JavaScript variables and functions. To avoid customizing PeopleTools, we will need to extract the relevant JavaScript and rewrite it in a standalone manner, meaning no dependencies on PeopleTools JavaScript. We'll start with a CSS selector to identify dots: .lpTabIndicators .dot. We will iterate over that dot collection, adding a click event handler to each dot. That click event handler needs to know the target tab on click so we need another CSS selector to identify tabs: .lpTabId span.ps_box-value. For compatibility reasons, I'm not going to use any libraries (like jQuery), just raw JavaScript:

(function () {
    "use strict";
    // include protection just in case PS inserts this JS twice
    if (!window.jm_config_dots) {
        window.jm_config_dots = true;
        var originalSetTabIndicators = window.setTabIndicators;

        // MonkeyPatch setTabIndicators because we want to run some code each
        // time PeopleSoft invokes setTabIndicators
        window.setTabIndicators = function () {

            // JavaScript magic to invoke original setTabIndicators w/o knowing
            // anything about the call signature
            var returnVal = originalSetTabIndicators.apply(this, arguments);

            // The important stuff that adds a click handler to the dots. This
            // is the code we want to run each time PS invokes setTabIndicators
            var dots = document.querySelectorAll('.lpTabIndicators .dot'),
                tabs = document.querySelectorAll('.lpTabId span.ps_box-value'),
                titles = document.querySelectorAll('.lpnameedit input');

            [].forEach.call(dots, function (d, idx) {
                d.setAttribute('title',titles[idx].value);
                d.addEventListener("click", function () {
                    lpSwipeToTabFromDD(tabs[idx].innerHTML);
                });
            });

            return returnVal;
        };
    }
}());

Funny... didn't I say we would start with a CSS selector? Looking at that code listing, that CSS selector and the corresponding iterator don't appear until nearly halfway through the listing. When testing and mocking up a solution, the objects you want to manipulate are a good place to start. We then surround that starting point with other code to polish the solution.

This code is short, but has some interesting twists, so let me break it down:

  • First, I wrapped the entire JavaScript routine in a self-executing anonymous JavaScript function. To make this functionality work, I have to maintain a variable, but don't want to pollute the global namespace. The self-executing anonymous function makes for a nice closure to keep everything secret.
  • Next, I have C-style include protection to ensure this file is only processed once. I don't know this is necessary. I suspect not because we are going to add this file using an event that only triggers once per component load, but it doesn't hurt.
  • The very next line is why I have a closure: originalSetTabIndicators. Our code needs to execute every time PeopleSoft invokes the delivered JavaScript setTabIndicators function. What better way to find out when that happens than to MonkeyPatch setTabIndicators? setTabIndicators creates those little dots. If our code runs before those dots are created, then we won't have anything to enhance. Likewise, when those dots change, we want our code to be rerun.
  • And, finally, redefine setTabIndicators. Our version is heavily dependent on the original, so first thing we want to do is invoke the original. As I stated, the original creates those dots, so it is important that we let it run first. Then we can go about enhancing those dots, such as adding a title and a click handler.

In Application Designer (or online in Branding Objects), create a new HTML (JavaScript) definition with the above JavaScript. I named mine JM_CLICK_DOTS_JS. You are free to use whatever name you prefer. I point it out because later we will reference this name from an Application Class, and knowing the name will be quite useful.

If we stop our UX improvements here, the dots will become clickable buttons, but users will have no visual indicator. It would be nice if the mouse pointer became a pointer or hand to identify the dot as a clickable region. We will use CSS for this. Create a new Free formed stylesheet for the following CSS. I named mine JM_CLICK_DOTS.

.lpTabIndicators .dot:hover,
.lpTabIndicators .dot.on:hover {
    cursor: pointer;
}

With our supporting definitions created, we can move onto Event Mapping. I detailed all of the steps for Event Mapping in my post Event Mapping: Extending "Personal Details" in HCM. According to that blog post, our first step is to create an App Class as an event handler. With that in mind, I added the following PeopleCode to a class named ClickDotsConfig in an Application Class appropriately named JM_CLICK_DOTS (noticing a pattern?). Basically, the code just inserts our new definitions into the page assembly process so the browser can find and interpret them.

import PT_RCF:ServiceInterface;

class ClickDotsConfig implements PT_RCF:ServiceInterface
   method execute();
end-class;

method execute
   /+ Extends/implements PT_RCF:ServiceInterface.execute +/

   AddStyleSheet(StyleSheet.JM_CLICK_DOTS);
   AddJavaScript(HTML.JM_CLICK_DOTS_JS);
end-method;

After creating a related content service definition for our Application Class, the only trick is choosing the Landing Page component content reference, which is a hidden content reference located at Fluid Structure Content > Fluid PeopleTools > Fluid PeopleTools Framework > Fluid Homepage

Probably the trickiest part of this whole process was identifying which event to use. For me the choice was between PageActivate and PostBuild. The most reliable way I know to identify the appropriate event is to investigate some of the delivered component PeopleCode. What I found was that the same code that creates the dots, PTNUI_MENU_JS, is added in PostBuild. Considering that our code MonkeyPatches that code, it is important that our code run after that code exists, which means I chose PostBuild Post processing.

Using Event Mapping we have effectively transformed a customization into a configuration. The point of reducing customizations is to simplify lifecycle management. We now have two fewer items on our compare reports. If Oracle changes PTNUI_MENU_JS and PTNUI_LANDING_CSS, the originally modified definitions, these items will no longer show on a compare report. But!, and this is significant: you may have noticed my JavaScript is invoking and manipulating delivered PeopleSoft JavaScript functions: setTabIndicators and lpSwipeToTabFromDD. If Oracle changes those functions, then this code may break. A great example of Oracle changing JavaScript functions was their move from net.ContentLoader to net2.ContentLoader. I absolutely LOVE Event Mapping, but we can't ignore Lifecycle Management. When implementing a change like this, be sure to fully document Oracle changes that may break your code. This solution is heavily dependent on Oracle's code but no Lifecycle Management tool will identify this dependency.

Question: Jim, why did you prefix setTabIndicators with window as in window.setTabIndicators? Answer: I am a big fan of JSLint/JSHint and want clean reports. It is easier to tell a code quality tool I'm running in a browser than to define all of the globals I expect from PeopleSoft. All global functions and variables are really just properties of the Window object. The window variable is a browser-defined global that a good code quality tester will recognize. The window prefix isn't required, but makes for a nice code quality report.