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.

11 comments:

Alan Northern said...

Jim, I came across this article while searching Event Mapping and thought I would give this a try as a practical example. However; I am experiencing unexpected results.

When I add the AddStyleSheet and/or AddJavaScript to my Event App Pkg code the Fluid Landing page does not render correctly (Fluid Menu shows all icons, Action list shows as bullet list, etc). In addition, it appears as PTNUI_MENU_JS is loading again outside of the PT_LANDINGPAGE PostBuild which sets the MonkeyPatched defined setTabIndicators function back to it's original code.

If I add the StyleSheet and JavaScript as the last lines of PostBuild the CSS works as expected but the setTabIndicators returns to it's default code line.

Jim Marion said...

@Alan, what tools release are you using? The latest versions of 8.55 and 8.56 should work. In prior releases, what I found is that Fluid would stop rendering any time I used AddJavaScript or AddStylesheet in Event Mapping, which is what it sounds like you are seeing. I confirmed the bug with PeopleTools before OpenWorld 2016, and have confirmed it with several customers since. I actually had this blog post in the queue for a while, but was waiting for the bug fix.

Alan Northern said...

Thanks for the response. We are currently on Tools version 8.55.12. Would this also contribute to the issue of PTNUI_MENU_JS apparently loading again after PostBuild causing the redefined setTabIndicators function to revert?

Jim Marion said...

@Alan, absolutely. If I remember right, 8.55.15 was the first release where I saw Fluid Event Mapping working correctly. I should have called that out in the initial post.

Unknown said...

When I tried to apply this feature, I got to the point where the hand appears by the dots. However, when I click on the dots, nothing happens. I am testing this with Tools 8.55.18 using chrome as the browser. Any ideas?

Jim Marion said...

@Lynn,

When you open the JavaScript console, do you see any errors? If not, do you see your JavaScript loaded in the network or sources tabs?

Sasank Vemana said...

I love the MonkeyPath idea. Very handy!

Pat said...

Hi Jim
Campus 9.2 Tools 8.56.01.
It appears the dots only work after the Choose Homepage selector has been clicked. Click a different homepage and they work. Click the PT_HOME home button again and they do not work.
No errors in console.
The JS is rendered to the browser.
I have tried both event mapping and Sasank's bootstrap injection method. Same result.

Jim Marion said...

@Pat, that surprises me. I tested this on 8.56.01 and 8.56.03. In your case, it appears the JavaScript isn't firing on first load. The question is "why?" to troubleshoot, I would start with some console.log statements. The first console.log statement would go right inside the self executing anonymous function. The second would go inside the monkeypatching code to confirm the JavaScript was patched.

Unknown said...

I'm on 8.55.19 and this does not seem to work. My Application Class is firing and loading in the javascript, however it seems that the PTNUI_MENU_JS is being loaded twice (I put an alert into the code). The second time it loads it replaces the monkeypatched function with the original. Any thoughts?

Jim Marion said...

@Joe, this may be a problem on 8.55. 8.56 allows us to map to PageActivate, which is the absolute last event to trigger, which should place our custom JavaScript at the very end of the call stack.