X

Technical Articles relating to Oracle Development Tools and Frameworks

  • JET
    February 9, 2017

JET Composite Components XIII - Patterns for Deferred and Asynchronous UI Loading

Duncan Mills
Architect

Introduction

This article outlines a series of simple approaches that you can use in Custom JET Components that do not have a fixed user interface. These might be UIs that are generated or altered dynamically based on the attributes defined for the component tag, or maybe based on data that the component itself is retrieving from a remote service as it starts up. In the article I will outline three basic patterns in order of complexity:

  1. Pattern 1 - Simple Conditional and Looping based UIs
  2. Pattern 2 - Consolidated Template based UIs
  3. Pattern 3 - Separately Templated UIs

In all cases, the view that ends up being used for the component shares the same basic component viewModel. All we are doing here is making the UI part a little more dynamic. These approaches can be useful when you want to display alternate views based on data that you simply don't have when the component is defined. For example, you may need the view to be sensitive to the runtime user role, the particular shape of a collection being displayed or even the type of device being used. Note that the patterns explained here can actually be blended together in various ways as well.

Pattern 1 - Simple Conditional and Looping based UIs

The pattern 1 approach simply leverages the power of basic conditional syntax to dynamically vary the UI. Specifically you can use conditional and looping constructs <oj-bind-if>, <oj-switch> and <oj-bind-for-each> within the basic view that you define for the component. The framework will automatically resolve these as the component has it's bindings applied. As a simple example to illustrate this, imagine that I have a component where I want to support two display modes for the consumer to be able to select from compact or full. To do this I could expose a compactView boolean property in my Custom Component metadata:

{
  "properties": {
    …,
    "compactView": {
      "description" : "Hides the First-Name if set to TRUE, defaults to FALSE",
      "type": "boolean",
      "value" : false
    }
  },
  …
}

Then the consumer can set the value to true if needed, thus:

<ccdemo-name-badge compact-view="true"  badge-name="..."/>

Then in my component view definition, ccdemo-name-badge.html I could use a <oj-bind-if> test to only display the first name data if the mode is not compact:

<div class="badge-face">
  <img class="badge-image" :src="[[$props.badgeImage]]" :alt="[[$props.badgeName]]"/>
  <oj-bind-if test="[[$props.compactView !== true]]>
    <h2><oj-bind-text value="[[upperFirstName]]"></oj-bind-text></h2>
  </oj-bind-if>
  <h3><oj-bind-text value="[[$props.badgeName]]"></oj-bind-text></h3>
</div>

Pattern 2 - Consolidated Template Based UIs

Leading on from the basic use of  inline tag base evaluation in the view HTML, we can take the logical next step of using the full Knockout template mechanism. The simplest version of this would be to define the alternative view templates inline in the base view HTML for the component. To show this, I'll use the same component compactView boolean property as before. The big alteration is in the component view definition (ccdemo-name-badge.html):

<div class="badge-face" data-bind="template: { name: viewModeTemplate}"/>
  <!-- Templates follow in-line -->
  <script type="text/html" id="compactTemplate">
    <img class="badge-image" :src="[[$props.badgeImage]]" :alt="[[$props.badgeName]]"/>
    <h3><oj-bind-text value="[[$props.badgeName]]"></oj-bind-text></h3>
  </script>
  <script type="text/html" id="fullTemplate">
    <img class="badge-image" :src="[[$props.badgeImage]]" :alt="[[$props.badgeName]]"/>
    <h2><oj-bind-text value="[[upperFirstName]]"></oj-bind-text></h2>
    <h3><oj-bind-text value="[[$props.badgeName]]"></oj-bind-text></h3>
  </script>

In this version of the HTML you can see that the main bulk of the component markup has been removed from the outer <div> and instead it has gained a template data-binding that uses a viewModel value called viewModeTemplate.

Additionally, the HTML has gained two scripts of type html/text called compactTemplate and fullTemplate respectively (based on their id attribute). These two scripts1 provide two alternative user interfaces that can be substituted into the main <div>.

The final ingredient to make this pattern work is the implementation of the viewModeTemplate property in the custom component viewModel. The Knockout template evaluation will expect this to contain a string which matches one of the available templates (e.g. "compactTemplate" or "fullTemplate"). In my example I'm triggering the change based on a boolean attribute called compactView. So in the property resolution for the component I can add a little logic to inspect that boolean value and store the appropriate template name into a property called viewModelTemplate. Here's the property resolution block2 with this added.

…
  function CCDemoNameBadgeComponentModel(context) {
    var self = this;
    context.props.then(function(propertyMap){
      //Save the resolved properties for later access
      self.properties = propertyMap;
      //Extract the badge-name value
      var badgeNameAttr = propertyMap.badgeName;
      self._extractFirstName(badgeNameAttr);
      //New code to select the correct template to use
      var compactMode = propertyMap.compactView;
      if (compactMode){
        self.viewModeTemplate = 'compactTemplate';
      }
      else {
        self.viewModeTemplate = 'fullTemplate';
      }
    });
 …

Pattern 3 - Separately Templated UIs

The final variation is naturally the most powerful but does involve a little more code. In this version, we'll still use the Knockout template mechanism, but rather than encoding the different template options into <script> tags within the view HTML we instead define totally separate HTML files for each UI variant desired. Using this approach we can actually remove the need for a placeholder HTML file and instead in-line that into the bootstrap loader.js. So in terms for files for our running sample composite component we might end up with:

/ccdemo-name-badge
      loader.js
      ccdemo-name-badge.json
      ccdemo-name-badge.js
      ccdemo-name-badge.css
      ccdemo-name-badge-compact.html
      ccdemo-name-badge-full.html

Next we make a slight alteration in the boostrap component.js so as to not inject an initial HTML view via the RequireJS text plugin:

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

Notice in the view property of the register() parameters I'm now injecting an HTML string directly and this encodes just a Knockout template reference. You'll also notice that this then supplies the nodes property of the template, not the name. (for more information about this see the Knockout doc). Injecting this HTML inline in this way simply removes the requirement to define a separate HTML file which would need to loaded in the define block as per the previous examples we've seen3.

Next we need to contrive how to get hold of the two possible template files within the component viewModel. The simplest approach here is to inject them through the define() block of the viewModel (although you could load them in other ways too, e.g. using require()). So amending our ccdemo-name-badge.js define block gives:

define(['ojs/ojcore','knockout','jquery',
        'text!./ccdemo-name-badge-compact.html',
        'text!./ccdemo-name-badge-full.html'
       ],
  function (oj, ko, $,compactTemplate,fullTemplate) {
  'use strict';
    …

Notice how the two HTML text streams are injected into the define function block as compactTemplate and fullTemplate respectively.

With this pattern, there is one more thing to do which is to set up the templateForView property that Knockout is expecting to contain the element subtree used to implement the template. We do this using the activated lifecycle method in the Composite Component. This will check our compactMode boolean property and then use the ko.utils.parseHtmlFragment() API to convert the correct template text stream into the subtree that Knockout expects:

CCDemoNameBadgeComponentModel.prototype.activated = function(context) {
  if (this.properties.compactMode){
    this.templateForView = ko.utils.parseHtmlFragment(compactTemplate);
  }
  else {
    this.templateForView = ko.utils.parseHtmlFragment(fullTemplate);
  }
};

There are many variations of this final pattern that you could use, including building a completely dynamic UI with no source HTML files on disk at all.

What's Next?

In the next article I take the idea of dynamic CCA content one step further by showing you how a custom component can use ojModule to present multiple views, each with their own viewModel.


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 Of course you are not restricted to just two alternative templates here, you can use as many as you like.

2 This is all using the same sample that I've been working with throughout this series of articles. Jump back to Part II if you've not encountered it before.

3 This does not stop you from doing so, however, it just depends on how you want to organize your code.

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