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.