Wednesday, February 11, 2015

Using RequireJS to Manage Dependencies

I have a handful of blog posts describing how to deal with JavaScript dependency conflicts such as multiple library inclusions, namespace conflicts, etc. These workarounds are necessary in portal environments that mash up content from various providers that may use the same libraries. On a portal (Interaction Hub) homepage, for example, you may have a Pagelet Wizard pagelet that uses the jQuery Cycle plugin as well as a Pagelet Wizard pagelet that uses jQuery UI. Both of these pagelets will include the jQuery library, but different jQuery plugins. As each pagelet loads, it will load jQuery and then its required plugins. Both pagelets will try to put $ and jQuery into the global (window) namespace. The last pagelet to load will reload window.$ and window.jQuery causing the browser to drop any previously loaded jQuery plugins.

One technique JavaScript developers use to manage dependencies in a scenario like this is to use RequireJS with Asynchronous Module Definitions (AMD). With RequireJS and AMD, you would define a RequireJS configuration file pointing to each JavaScript library and plugin and then write JavaScript that uses these libraries with a RequireJS closure. This approach keeps custom libraries out of the global namespace and ensures that libraries only load once (not once for each pagelet). PeopleTools 8.54 makes implementing this really easy through the new Branding Objects module and Branding System Options. Let's review an example. Let's say that I have RequireJS, jQuery, and jQuery UI loaded into JavaScript objects in the Branding Objects module as shown in the following image

Now let's say you have your Branding System Options configured to include RequireJS and the RequireJS configuration file as described in the following screenshot:

You could then create a Pagelet Wizard pagelet containing HTML like the following and not worry about dependencies or namespace pollution. Everything would just work

<div id="NAA_DIALOG_TEST_html">
  <style type="text/css">
    @import url(%StyleSheet(NAA_JQ_UI_1_11_2));
    #NAA_DIALOG_TEST_html .dialog { display: none };
  </style>
  <script>
      require(['jquery', 'jqueryui'], function ($) {
        $(document).ready(function() {
          console.log("dialog pagelet is using version " + $.fn.jquery);
          $("#NAA_DIALOG_TEST_html").find("button")
              .button()
              .click(function( event ) {
                event.preventDefault();
                $("#NAA_DIALOG_TEST_html .dialog").dialog();
              });
          });
      });

  </script>
  <button>Show Dialog</button>
  <div class="dialog" title="Basic dialog">
    <p>This is the default dialog which is useful for displaying information.
    The dialog window can be moved, resized and closed with the 'x' icon.</p>
  </div>
</div>

Of course, this assumes that your RequireJS configuration file looks something like this:

/**
 * RequireJS global configuration. Include after RequireJS in branding settings
 * 
 * @returns {undefined}
 */
(function () {
  /**
   * Build a URL based on the current component's URL
   * @param {type} scriptId
   * @returns {String} derived URL for JavaScript
   */
  var getScriptUrl = function (scriptId) {
    var mainUrl = /*window.strCurrUrl ||*/ window.location.href;
    var parts =
        mainUrl.match(/ps[pc]\/(.+?)(?:_\d)*?\/(.+?)\/(.+?)\/[chs]\//);
    return window.location.origin + "/psc/" + parts[1] + "/" + parts[2] +
        "/" + parts[3] +
        "/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=" + scriptId;
  };

  require.config({
    paths: {
      /* Using non-standard name because 1.6.2 is not AMD compliant whereas
       * later versions are compliant. Don't want conflict with later version
       */
      'jquery': getScriptUrl("NAA_JQ_1_11_2_JS"),
      'jqueryui': getScriptUrl("NAA_JQ_UI_1_11_2_JS"),
      'jquery-private': getScriptUrl("NAA_JQ_PRIVATE_JS")
    },
    map: {
      // '*' means all modules will get 'jquery-private'
      // for their 'jquery' dependency.
      '*': { 'jquery': 'jquery-private' },

      // 'jquery-private' wants the real jQuery module
      // though. If this line was not here, there would
      // be an unresolvable cyclic dependency.
      'jquery-private': { 'jquery': 'jquery' }
    }
  });
}());

And your jQuery-private module looks something like this:

// http://requirejs.org/docs/jquery.html#noconflictmap
define(['jquery'], function (jq) {
    return jq.noConflict( true );
});

What's up with the getScriptUrl function? JavaScript HTML definitions do not yet support %JavaScript Meta-HTML. The getScriptUrl JavaScript function attempts to perform the same task, but using client-side JavaScript.

Why do we need a jquery-private module? The point is to hide all of our dependencies and just expose them within the RequireJS closure. That way we avoid conflicts with older code that uses jQuery as well as any PeopleTools delivered JavaScript that may user

This technique also works well for loading dependencies. I often use a JavaScript library in a pagelet, with JavaScript executed directly in the pagelet. One challenge I have had is ensuring that my browser parses and processes any JavaScript libraries before JavaScript embedded in a pagelet. RequireJS solves this by first loading the dependencies, and then executing the JavaScript within the define/require function.

Note: For this to work properly, it is important that your JavaScript libraries are either AMD compliant or can be appropriately shimmed. Current versions of jQuery and jQuery UI are AMD compliant. The older version of jQuery UI that ships with PeopleTools 8.54 and earlier is NOT AMD compliant. Instead, I downloaded the latest jQuery UI and uploaded it using the new Branding Objects module. To work successfully, the jQuery UI CSS must be updated to use %Image() for each image resource and each image must be uploaded using the Branding Objects component.

39 comments:

Mani S said...

Hi Jim,
Sorry for posting off-topic.. For a field to appear in the Basic search page of a field with a prompt, we would check Default Search field in the Record field properties. Is there any specific condition for that field to be marked as a Default Search Field? Because, I have a custom table where SETID and CUST_ID are key fields and there are numerous ALt Key fields. I would like one of these Alt Key fields to appear when in Basic Search mode. Is there anyway this can be achieved or am I missing something?

Jim Marion said...

@Mani, you may want to post that question on the PeopleSoft General Discussion OTN forum.

Mani S said...

Thanks, Jim.. we've finally resorted to another way to get this to work. Quick question though - is it possible to set search mode - Basic or Advanced - at a page field level via PeopleCode?

Jim Marion said...

@Mani, search mode is determined at the component level.

Unknown said...

Hi Jim We are in the process of implementing Interaction Hub 9.1 R2 (tools 8.53); Trying to replicate the look&feel of a legacy portal with multiple tabs on home page. One big issue is to make Jquery available on the page to enable hover menu and other custom objects that use Javascripts. Jquery linbrary(downloaded from jquery.org) was placed in 'PS' folderin the root folder of web server. We tried to reference is by"/ps/jquery-min.js' , 'ps/jquery-ui.min.js' and 'jquery-ui-custom.min.js' but that did not work. Can you please guide as to how to invoke the jqyer library from the header? Thank you so much! - Captain

Jim Marion said...

@Captain Sachi, it doesn't look like you tried /jquery-ui.min.js. If you put it in the root of the web server, then that is the path.

Vlad said...

Jim, thanks for the guide. I have one note regarding the requireJS configuration. If your configuration was to be run in IE9, the generated URL for the script location will be broken as IE9 does not support the window.location.origin property. You can alleviate this by including the below code after the parts variable and before the return:

if (!window.location.origin) {
window.location.origin = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: '');
}

I'm not sure if there are cases where this, too, would fail, but it should at least get IE9 users going with RequireJS.

Jim Marion said...

@Vlad, good point and thank you for the shim.

Now that I think about it, you probably don't need the fully qualified URL. A relative URL starting with /psc is probably good enough and we could leave off the whole protocol, server, port stuff.

Unknown said...

Hi Jim,

It seems PS does not allow you to set the HttpOnly attribute on cookies? You can set it on the web server session cookie, but not the others (PS_LOGINLIST, expirePage, PS_TOKEN, etc)This came up in a security audit recently and it looks like our options are pretty limited on addressing it. Is it by design since PS itself accesses the cookies via JS? Any input is appreciated, thanks!

Jim Marion said...

@Ryan, that is a great question for MyOracle support. I am not aware of any reason these cookies should be accessible to JavaScript. Good catch.

Unknown said...

Thanks Jim. I found a previous bug (11521341) related to this, but didn't see a resolution. I'll follow up with Oracle support as you suggest. Thanks again.

Unknown said...

Hi Jim - Just as an FYI, here was Oracle Support's response. I'm still a little curious as to why HttpOnly cannot be set on the other cookies, but at least I have something for our Security team. And sorry for the off-topic post.

1. HTTPOnly is needed only for session cookies.

2. So we are setting this for session cookies by default. you need not do any settings for it. (ex: XYZ-1234-PORTAL-PSJSESSIONID)

3. Since the other cookies you will see in a peoplesoft session are not the webserver session cookies, the HTTPOnly is not needed .

4. So we do not set this for other cookies and it is not possible to do so.

Unknown said...

Hi Jim,

Great post, works like a charm!

I came across this small thing:

I published the pagelet on my homepage and see the button. When I click on it, the Dialog pops-up. So far so good!

Then I close the dialog and click the button again.. This time the dialog doesn't pop-up.

It appears that I have to refresh the pagelet first before the button triggers the dialog to pop-up again. Tested on Firefox and Chrome.. Maybe Someone has this same problem?

Thanks!

Jim Marion said...

@Stefan, Add some JavaScript to print to the console to see if your dialog is really setting itself to visible. Are you using jQuery UI dialogs or something different? I had a problem with jQuery UI dialogs and had to destroy them on close (hide) and recreate them each time I wanted to show a dialog.

Jim Marion said...

@Stefan, Add some JavaScript to print to the console to see if your dialog is really setting itself to visible. Are you using jQuery UI dialogs or something different? I had a problem with jQuery UI dialogs and had to destroy them on close (hide) and recreate them each time I wanted to show a dialog.

Unknown said...

Hi Jim, thanks for your feedback!

I used exact the same example as you posted on your blog (with jQueryUI dialogs).

I tried several options, but the one that worked for me was removing the #NAA_DIALOG_TEST_html in the selector area. Probably there are more ways to achieve this, but this was a pretty simple adjustment.


$("#NAA_DIALOG_TEST_html").find("button")
.button()
.click(function( event ) {
event.preventDefault();
$("#NAA_DIALOG_TEST_html .dialog").dialog();
});
});

changed it to:


$("#NAA_DIALOG_TEST_html").find("button")
.button()
.click(function( event ) {
event.preventDefault();
$(".dialog").dialog(); /*changed*/
});
});

Hopefully this is useful to somebody
Now the dialog behaves like it should. Thanks for helping out!

Kevin Weaver said...

Jim,

I am trying to get this to work for me on a homepage in Interaction Hub using Pagelets that all require JQuery and come from our HCM system. I have created the branding objects and configured them using the Branding Sytem Options in HCM, but they don't show up in our Portal.
So I figured that I must need them in Interaction Hub, so I configured them there and still they don't show up on the homepage?

Any thoughts?

We are tools 8.54.09, if that helps...

Jim Marion said...

@Kevin, when you say, "they still don't show up," are you referring to the RequireJS and configuration file directly or the libraries included by RequireJS? Did you look at the network tab of your browser tools to make sure everything is being downloaded? Do you have any errors in the console that might help you identify what is wrong?

Kevin Weaver said...

The RequireJS and the Configuration file don't show up in my Interaction Hub. After I configured them in my HCM environment, I could see them load there, but not in the Hub. I am being told now that we might have a bug fix the needs to be applied for Branding in Portal. So maybe that might fix this issue. Is my approach correct? Should I be configuring this in Interaction Hub or HCM? My initial thoughts was HCM, but when it did not work I started questioning myself. Now I am confused. Thanks for your help!

Kevin

Jim Marion said...

@Kevin,

Homepages load from the server identified in the URL, not from content providers. If you are logged into HCM, then you need to configure this in HCM. If you log into Interaction Hub, then you need this configured in Interaction Hub. The best approach is to configure it in both. Then when you import a pagelet into Interaction Hub from HCM, the pagelet can just use require(...) and let the container resolve library dependencies.

Kevin Weaver said...

That is the information I was looking for, thanks! I have been doing some testing of the branding in Portal and I have discovered that the branding works on the default homepage, but I have another tab that it does not work. Is there something I need to configure on the tab to make it work?

Thanks!

Jim Marion said...

@Kevin, regarding branding, that is odd. I don't know why it would behave that way.

Kevin Weaver said...

I will document the issue and open a service request.

Thanks!

Kevin Weaver said...

Jim,

I have discovered that the Branding does not work if there are no pagelets on the homepage. Does this mean that the branding will only work within components? The homepage where I wanted to use it is built with 1 navigation collection and 5 iscript based pagelets. Hence, no branding objects. Is this how it is suppose to work?

Thanks!

Kevin Weaver said...

Jim,

I just tested a homepage in my 9.2 hcm with no pagelets and the branding worked. So I think we have something wrong with our Interaction hub.

Thanks for you time!

Jim Marion said...

@Kevin, I did not realize that. I checked and I have company directory on my homepage, so that could explain it.

Jim Marion said...

@Kevin, the key is getting RequireJS to load. There are a couple of ways to load RequireJS on a homepage. One of them is to create your own attribute based branding theme. The problem with this approach is that a homepage with a component would have RequireJS twice, but this may not be an issue. Never tried it.

That was the whole point of this blog post: Pagelet Wizard and homepages working the same. So much for that idea.

Jim Marion said...

@Kevin, I did some research on this. I removed all component based pagelets from my homepage and RequireJS was still there, meaning it still works. So I asked development what to expect and one tip I received is to check the PeopleTools options in PeopleTools | Portal | Branding | Branding System Options. If the Application Package is PTBR_Branding, then it should work. If you applied an upgrade, however, the value may still be the old PT_BRANDING. I am using a PUM image, which has PTBR_BRANDING and that may be why it works for me and not for you. If it is still PT_BRANDING, then you should see RequireJS only if you have a component pagelet on the homepage.

Kevin Weaver said...

That worked! However, I really don't like the styles on the homepage tabs. There is a lot of space between them and they don't have rounded edges. Now I need to research how to modify the css.

Thanks Jim!

Kevin Weaver said...

I am getting a JavaScript error in the jquery private,

TypeError: jq is undefined


return jq.noConflict( true );});

Any idea what I am doing wrong?

Kevin Weaver said...

Nevermind, I fat fingered my jquery html name. :)

Kevin Weaver said...

Looks like I am dominating all the comments on this entry. My latest issue is that I am trying to use this for either colorbox or fancybox. But I keep getting a $().fancybox or $().colorbox is not defined. I was reading how to make the colorbox dependent on the jquery, and I modifed the configuration js to incude this:

shim: {
'colorbox' : { deps: [ 'jquery' ]}
}

Do you have any idea on how to make this work for either of fancybox or colorbox?

Thanks Jim!

Jim Marion said...

@Kevin, I assume that $ is your variable in your require closure/callback function? I also assume that fancybox and/or colorbox are require elements at the end of the require array and before the function definition? If fancybox and colorbox aren't AMD modules, then you may also be having a problem because fancybox and colorbox are trying to register themselves into a global window.jQuery, which may not exist. This problem is caused by using jquery-private to keep jQuery out of the global scope.

Kevin Weaver said...

That must be my problem. I don't find many references of using fancybox or colorbox in combination with requirejs. Is the jQuery UI's dialog box the equivalent to other jQuery APIs like colorbox? Can I use dialog to serve psc content? Thanks Jim! You are a huge help!

Jim Marion said...

@Kevin, I use jQuery UI Dialog to serve /psc/. The difference between jQuery UI Dialog and others, however, is that jQuery UI dialog doesn't let you specify a URL. Instead, you have to specify a div with an iframe. What I usually do is use JavaScript to append a div with a specific ID containing an iframe and then I that as the base for the dialog. I have my own dialog function that receives a URL and title. I use that to build the dialog. It isn't as easy as the plugins.

Kevin Weaver said...

Thanks for your help, I got this all switched over to the requirejs using the jQuery UI dialog and it works great. And now I don't have any dependencies on my custom dashboard. I do however have one question about branding that is not related to requirejs. We want to change the Branding Logo and I noticed that the branding logo is defined within the new stylesheet.

Like this:

/* company logo div */
#pthdr2logoswan {
float:%AlignStart;
width: 113px;
height: 55px; /* new for hover menu */
margin:0;
/*position:relative;
top:-29px;*/
padding: 0 5px 0 0;
background:none;
}
#pthdr2logoswan:before {
content: url(%Image(PT_ORACLELOGO_CSS));
left: 4px;
position: relative;
top: 17 px;
}


What is the best practic for updating this to our company's logo.

Thanks Jim!

Jim Marion said...

@Kevin, the recommendation hasn't changed much. Create a new free formed stylesheet and add the #pthdr2logoswan:before with new attributes. Be sure to further qualify your selectors so your new selector wins the specificity test. The recommendation is to add this CSS to your attribute based branding theme so you can change logos by theme.

Kevin Weaver said...

Thanks Jim, I think we are just about ready to go live with HCM 9.2 and Interaction Hub!

Hari said...

Hi Jim, need your help to get an idea on the below issue that happens on mobile devices.

we are in Tools version 8.54.

We are presenting an Employee name link in a page (this page is designed in dynamic HTML script). Once user clicks on a link we are showing a search page as a pop up(child page -this is fluid page designed in app designer))

To open the search page we are using window.open method in Javascript.
Once user select name from search and click ok button, selected name will be copied to parent page and then child pop up page will be closed (using self.close method).

This working fine in browsers, even in mobile. But when user saves the URL of this transaction as an APP, it fails to close the child window as it doesn't recognize as a page.

Could you please provide an idea how to close a child window when its being opened from APP??

Thanks,
Hari.A