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

Last Updated Jan 2020 for JET 8

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 two basic patterns in order of complexity:

  1. Pattern 1 - Simple Conditional and Looping based UIs
  2. Pattern 2 - Template based 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-view.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="[[$properties.badgeImage]]" 
       :alt="[[$properties.badgeName]]"/>
  <oj-bind-if test="[[$properties.compactView !== true]]>
    <h2><oj-bind-text value="[[upperFirstName]]"></oj-bind-text></h2>
  </oj-bind-if>
  <h3><oj-bind-text value="[[$properties.badgeName]]"></oj-bind-text></h3>
</div>

Pattern 2 - Template Based UIs

Leading on from the basic use of inline tag based evaluation in the view HTML, we can take the logical next step of using the <oj-bind-dom>  templating mechanism to take a completely separate fragment of HTML and insert it into the component view at some suitable point.  To show this, I'll use the same component compactView boolean property as before, but now we have some additional artefacts. First of all let's define a couple of HTML template files that will provide the alternate fragments that I want to inject based on the value of compactView. I'll create these both in a subdirectory of my component distribution called /templates.

/templates/compact.html

<h3>
  <oj-bind-text value="[[badgeName]]"></oj-bind-text>
</h3>

/templates/full.html

<h2>
  <oj-bind-text value="[[upperFirstName]]"></oj-bind-text>
</h2>
<h3>
  <oj-bind-text value="[[badgeName]]"></oj-bind-text>
</h3>

Next a new version of the core view HTML which uses <oj-bind-dom> to inject the variable part of the content from the correct template into the desired position.

ccdemo-name-badge-view.html

<div class="badge-face">
  <img class="badge-image" 
       :src="[[$properties.badgeImage]]" 
       :alt="[[$properties.badgeName]]">
  <oj-bind-dom config="[[nameAreaConfig]]"></oj-bind-dom>
  <div class="greeting-area">
    <oj-bind-slot name="greetingArea">
      <!-- Default Content -->
      <span>Hi</span>
    </oj-bind-slot>
  </div>
</div>

So, now we have two alternate templates (compact and full) that can be injected into the correct spot within the updated component view.  You can see that the oj-bind-dom tag has a config property that is bound to a configuration object (nameAreaConfig).  That, of course, needs to be defined within the viewModel file for the component so let's look at the changes there as well ( I'm just showing the start of the file for brevity - the rest is unchanged from the version used earlier in the series), 


define(
  ['knockout',
    'ojL10n!./resources/nls/ccdemo-name-badge-strings',
    'ojs/ojcontext',
    'ojs/ojhtmlutils',
    'text!./templates/compact.html',    
    'text!./templates/full.html',
    'ojs/ojbinddom',
    'ojs/ojknockout',],
  function (ko, componentStrings, Context, HtmlUtils, compactTemplate, fullTemplate) {

    function CCDemoNameBadgeComponentModel(context) {
      var self = this;

      self.composite = context.element;
      self.properties = context.properties;
      //Extract the badge-name value
      var badgeNameAttr = self.properties.badgeName;
      self.upperFirstName = ko.observable(self._extractFirstName(badgeNameAttr));
      // Sort out the dynamic part of the DOM
      var fragmentToInsert = self.properties.compactView?compactTemplate:fullTemplate;
      self.nameAreaConfig = ko.observable(
        {
          view: HtmlUtils.stringToNodeArray(fragmentToInsert), 
          data : {upperFirstName:self.upperFirstName, 
                  badgeName: self.properties.badgeName}
        }
      );
    };
 ...
);

In this version of viewModel there is a fair bit to unpack:

  1. Import of ojs/ojhtmlutils - this class provides a handy utility function that will be used to create actual DOM nodes from the template that has been selected
  2. Imports of compact.html and full.html - notice how I import these templates from the desired location using the requireJS text! plugin.  this will just read the contents of the HTML as a string, hence the need for the function from HtmlUtils to do the conversion into real DOM nodes for me.
  3. Import of ojs/ojbinddom which is need for the oj-bind-dom tag to work 
  4. The constructor code selects the required template by checking the compactView property value
  5. The nameAreaConfig variable is created as an observable to drive the <oj-bind-dom>. 

The key part here is, of course the contents of the nameAreaConfig object.  This has two top level attributes - the view which has to be a collection of DOM nodes (created from the selected template using the HtmlUtils.stringToNodeArray() function)  and then a data object which defines the actual data attributes which will be available for the template fragment to use. In this case I have made attributes called upperFirstName and badgeName available within the template. You can expose any data you want here, even this/self.

This approach of using oj-bind-dom is super-powerful and flexible.  You could, for example, dynamically build the DOM node subtree in code rather than reading markup from template files, or you could use separate template files but load them in a lazy fashion using require() as opposed to the define() block on the viewModel.

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

 

 

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.