Last Updated Feb 2022 for JET 12
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
