X

Technical Articles relating to Oracle Development Tools and Frameworks

  • JET
    February 8, 2017

JET Custom Components XII - Revisiting the Loader Script

Duncan Mills
Architect

Introduction

Just to complete the core set of technical articles on custom components, before I move onto the more use-case driven topics, I wanted to revisit the loader script. Recall that way back in Part II of the series I mentioned that the the loader script was mostly boilerplate and unchanging apart from perhaps the injected script names? Well that's certainly mostly true, but there are some tweaks and advanced options to discuss before we can say that the topic is totally complete. So here I want to look at three things:

  1. Custom property parsing
  2. Options for Register Configuration Properties
  3. Loading remote modules

Note there have been some significant changes in this area for JET 5.0.0 and above. I have left this article intact for historical reference and for those of you working on older versions but do note the following and amend your coding appropriately if you are using JET 5+

  1. The register command no longer needs to take an object with an inline or promise attribute.
  2. The promise option has been removed all together and so techniques using that will not work in JET 5.0.0 or above

Custom Property Parsing

One of the advanced capabilities of the loader script is to supply a custom parsing function for properties as part of the oj.Composite.register(...) call. I've included a mention of it for completeness, however, it was the subject of a whole article on it's own, so if you need to learn about this, jump back to Part X to read all about it.

Options for Register Configuration Properties

If we look at a typical loader script, you will note that the various configuration properties for the composite are marked with inline here for example is the one that I've been using throughout this series for the ccdemo-name-badge component.

define(
  ['ojs/ojcore',
   './ccdemo-name-badge',
   'text!./ccdemo-name-badge.html',
   'text!./ccdemo-name-badge.json',
   'css!./ccdemo-name-badge',
   'ojs/ojcomposite'],
   function (oj, ComponentModel, view, metadata) {
   'use strict';
      oj.Composite.register('ccdemo-name-badge',
        {
            metadata: {inline: JSON.parse(metadata)},
            viewModel: {inline: ComponentModel},
            view: {inline: view}
        });
   }
);

The use of inline here simply indicates to the framework that the value for the property is right here and no promise resolution is required to use it. You can also literally define the property contents "inline" should you wish, although the readability of your definition might suffe.

That aside, the implication is that the various values for these configurations do not have to be supplied via the define block. If you want to obtain your viewModel (for example) in some other way, say using a factory, then that would be fine. In an extreme case we might not use the define block for any direct injection at all:

define(
  ['ojs/ojcore',
   'plugins/ModelFactory'
   'ojs/ojcomposite'],
   function (oj, factory) {
   'use strict';
      var metadata = {"name" : "ccdemo-name-badge",
                      "version" : "1.0.0",
                       ...};
      var viewHTML = '<div><h2>Test Composite</h2>...';
      oj.Composite.register('ccdemo-name-badge',
        {
            metadata: {inline: metadata},
            viewModel: {inline: factory.makeModel()},
            view: {inline: viewHTML}
        });
   }
);

I'd really not encourage over-use of the loader script in this way, unless the configurations are super simple, otherwise things just get hard to read and you may breat compatibility with any future tooling that is expecting a more standard file layout format. But the principle stands, that the register() API configuration properties are not magical, you can populate them in any way that you choose.

The use of factories for creating the viewModels and views may be the most likely type of deviation from the standard pattern. To help support this further, the register() API configuration properties can also be provided as {promise:...} as an alternative to {inline:...}. As you would expect, this instructs the CCA that the value for the property is supplied as a promise and internally it will wait for the promise to resolve before the registration is completed. So for example we might use the requireJS require() API to load a view from some dynamic location 1:

var viewPromise = new Promise( function(resolve, reject){
  var viewLocation = '/js/random/';
  require(['text!' + viewLocation + 'ccdemo-name-badge.html'],
          function(resolvedView){
            resolve(resolvedView);
          };);
  });
  oj.Composite.register('ccdemo-name-badge',
    {
        metadata: {inline: JSON.parse(metadata)},
        viewModel: {inline: ComponentModel},
        view: {promise: viewPromise}
    });

It goes without saying that you should not define both inline:... and promise:... for the same configuration property.

Remote Module Loading

On the subject of the loading of component constituents, ie the view, viewModel etc., we need to think about the case where loading is remote to the consuming application.

As a matter of course, your loader script (e.g. ccdemo-name-badge/loader.js in my case) should load all of it's resources using relative paths in the loader define block. This is what we have been doing all along. By sticking to relative paths in the loading, the only requireJS configuration needed on the part of the consumer is to define the home / root location of the Composite Component.

Right? Well almost. That's fine if the component has been sucked into the consuming application codebase. If you wanted to load the component from a remote location, however, for example a CDN, then here is one additional piece of configuration for the consumer. This is actually nothing to do with the CCA per say, but rather relates to the default behavior of requireJS when loading from separate origins 2. By default, the requireJS text plugin will, as a security measure, not allow resources (such as the view and metadata) to be fetched. Only .js files will work from a different origin.

To fix this is a simple configuration task in the requireJS configuration of the consuming application. The configuration for the text plugin should define a useXhr function to return true for the source server for the component.

Here's an example requireJS configuration for an application that implements the most general version of this:

requirejs.config(
  {
    baseUrl: 'js',
    // Path mappings for the logical module names
    paths:
    {
      'knockout': 'libs/knockout/knockout-3.4.0.debug',
      'jquery': 'libs/jquery/jquery-3.1.0',
      'jqueryui-amd': 'libs/jquery/jqueryui-amd-1.12.0',
      'promise': 'libs/es6-promise/es6-promise',
      'hammerjs': 'libs/hammer/hammer-2.0.8',
      'ojdnd': 'libs/dnd-polyfill/dnd-polyfill-1.0.0',
      'ojs': 'libs/oj/v2.2.0/debug',
      'ojL10n': 'libs/oj/v2.2.0/ojL10n',
      'ojtranslations': 'libs/oj/v2.2.0/resources',
      'text': 'libs/require/text',
      'css' : 'libs/require-css/css',
      'signals': 'libs/js-signals/signals'
    },
    // Shim configurations for modules that do not expose AMD
    shim:
    {
      'jquery':
      {
        exports: ['jQuery', '$']
      }
    },
    ,
    //Additional Configuration
    config:
    {
      text:
        {
          useXhr: function (url, protocol, hostname, port) {
            // Allow cross-domain requests to get Text resources
            // Remote server must set Access-Control-Allow-Origin header
            return true;
          }
        }
    }
  }
 );

Note the implementation here is wide open because it blindly returns true from the useXhr function. In reality the consumer should only return true if the load is from a trusted source as determined from the supplied hostname etc.

What's Next?

Over the past 12 articles I've covered the core knowledge-base needed to create your own JET Custom Components to quite a sophisticated level. The following articles in this series are more focused on examining specific use cases and techniques that I've encountered when working with this technology. The next article will specifically be looking at how to create User Interfaces within your components on the fly rather than having them all pre-defined as part of the component definition.


JET Custom Component Series

If you've just arrived at Custom JET Components and would like to learn more, then you can access the whole series of articles on the topic from the Custom JET Component Learning Path


1 Note that I'm not recommending this as a way to dynamically load variable views, that's a subject of the next article.

2 I should point out that this is in addition to any CORS configuration.

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha