X

Technical Articles relating to Oracle Development Tools and Frameworks

  • JET
    February 13, 2017

JET Custom Components XIV - A Pattern for Multi-view Components using ojModule

Duncan Mills
Architect

Last Updated Jan 2020 for JET 8

Introduction

In general, one would expect components to be fairly limited in scope and only have to define a single UI (albeit one that might be reasonably dynamic as discussed in this previous article). However, the occasion may arise where the component needs to be more complex and maybe needs some scoped sub-views. A good example of this might be a wizard-style UI in which you need to encapsulate several distinct "pages" to step though, into a single component.

This article outlines an approach you can take to implementing this kind of custom component by leveraging the capabilities of JET modules (<oj-module>). I would emphasize that this approach is only needed when your sub-views within the custom component all need their own viewModels. If all you want to do is define a number of views all based off of the Component viewModel then you can stick to the methods discussed in the previous article to switch the UI.

The Scenario

For this scenario I want to package up the module views and viewModels as part of the Composite Component. So for example I might have the following file structure:

/js
  /jet-composites
    /ccdemo-wizard
      loader.js
      component.json
      ccdemo-wizard.js
      ccdemo-wizard.css
      ccdemo-wizard.html
      /modules
        /wizard-page-1
          view.html
          viewModel.js
        /wizard-page-2
          view.html
          viewModel.js
        /wizard-page-3
          view.html
          viewModel.js 

Implementation

Let's step through getting all this working.

Step 1 - Define the Core Composite Component

To illustrate this I'll start off with a basic component called ccdemo-wizard here's the metadata:

{
  "name":"ccdemo-wizard",
  "version" : "1.0.0",
  "jetVersion":"^8.0.0",
  "events" : {
    "wizardCancelled": {
      "description" : "Called if the user presses the Cancel button in the wizard",
      "bubbles" : true,
      "cancelable" : false
    },
    "wizardComplete": {
      "description" : "Called if the user presses the Finish button in the wizard",
      "bubbles" : true,
      "cancelable" : false
    }
  }
}

As you can see it's pretty simple and really only defines two events to allow the consumer to detect the overall state of the wizard. The component will also have a very standard loader script (loader.js) and style sheet (ccdemo-wizard-styles.css). There is nothing unusual about these so I've not reproduced them here.

Step 2 - The Component View Template

Next, the HTML template for the wizard component itself (ccdemo-wizard-view.html). This just contains a module binding and the buttons to control the paging though the wizard and to raise the wizardCancelled or wizardFinished events: I've formatted the oj-module definition for more readability:

<div class="oj-panel oj-panel-alt5">
  <oj-module config="[[
     ModuleElementUtils.createConfig({
       viewPath: 'ccdemo-wizard/modules/wizard-page-' + currentPage() +'/view.html',
       viewModelPath: 'ccdemo-wizard/modules/wizard-page-' + currentPage() +'/viewModel'
     })]]">
  </oj-module>
  <div>
    <oj-button on-oj-action="[[previousPage]]">Previous</oj-button>
    <oj-button on-oj-action="[[nextPage]]">Next</oj-button>
    <oj-button on-oj-action="[[cancelWizard]]">Cancel</oj-button>
    <oj-button on-oj-action="[[finishWizard]]">Finish</oj-button>
  </div>
</div>

So the key thing here is the use of the <oj-module> component to represent the wizard "pages".  This is controlled / configured by the config property which in turn uses a utility function called ModuleElementUtils.createConfig() to create an ojModule configuration  based on the view and viewModel of the selected wizard page which is identified by the currentPage observable that the viewModel for the component creates. Notice how the path to the view and viewModel for the configuration uses the requireJS path of the component itself as the root for finding the individual modules and the rest of the path is "calculated" based on the currentPage value.  

Note I've chosen to create the configuration inline within the HTML - you would equally manage the module configuration object from the component viewModel

Step 3 - The Component ViewModel

I'll start off by listing the code for the Composite Component viewModel and then I'll point out the important bits:


define(
  ['knockout', 
   'ojL10n!./resources/nls/ccdemo-wizard-strings', 
   'ojs/ojcontext',
   'ojs/ojmodule-element-utils', 
   'ojs/ojmodule-element', 
   'ojs/ojknockout', 
   'ojs/ojbutton'], 
   function (ko, componentStrings, Context, ModuleElementUtils) {

    function CCDemoWizardComponentModel(context) {
      var self = this;
      self.composite = context.element;
      self.properties = context.properties;
      self.res = componentStrings['ccdemo-wizard'];
      self.ModuleElementUtils = ModuleElementUtils;
      self.currentPage = ko.observable(1);    
    };

    CCDemoWizardComponentModel.prototype.previousPage = function (event, vm) {
      var self = vm;
      if (self.currentPage() > 0){
        self.currentPage(self.currentPage()-1);
      }
    }

    CCDemoWizardComponentModel.prototype.nextPage = function (event, vm) {
      var self = vm;
      if (self.currentPage() < 3){
        self.currentPage(self.currentPage()+1);
      }
    }

    CCDemoWizardComponentModel.prototype.cancelWizard = function (event, vm) {
      var self = vm;
      var eventParams = {'bubbles': true,'cancelable': false};
      //Raise the custom event
      self.composite.dispatchEvent(new CustomEvent('wizardCancelled', eventParams));      
    }

    CCDemoWizardComponentModel.prototype.finishWizard = function (event, vm) {
      var self = vm;
      var eventParams = {'bubbles': true,'cancelable': false};
      //Raise the custom event
      self.composite.dispatchEvent(new CustomEvent('wizardComplete', eventParams));
    }

    return CCDemoWizardComponentModel;
  });

Let's break that down:...

First we import the ojs/ojmodule-element-utils class. This is used to provide a convenience method used by the HTML View to generate a configuration for the module. Notice how the reference to this is stored on the viewModel instance so as to make the ModuleElementUtils reference accessible to the view.

Second we import the ojs/ojmodule-element class which is required for the <oj-module> tag to function

Finally the constructor defines the currentPage() observable on the instance and initializes it to point to the first page.  The value of this observable is then updated by the event handlers for the next and previous buttons.  Because this is an observable the configuration will be automatically "recalculated" whenever currentPage changes - doing the navigation between modules in the process.

This then is a core pattern to follow if you need to create these sorts of multi-module Composite Component. There are, of course, many possible variations in the way that this pattern can be used or adapted, but this is a good baseline.


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.