Introduction
This article outlines a series of simple approaches that you can use in JET Composite 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 Composite Component Tag, or maybe based on data that the Composite itself is retrieving from a remote service as it starts up. In the article I will outline three basic patterns in order of complexity:
- Pattern 1 - Simple Conditional and Looping based UIs
- Pattern 2 - Consolidated Template based UIs
- Pattern 3 - Separately Templated UIs
Pattern 1 - Simple Conditional and Looping based UIs
The pattern 1 approach simply leverages the power of basic Knockout syntax to dynamically vary the UI. Specifically you can use conditional and looping constructs such as if, visible and forEach within the basic view that you define for the Composite Component. The framework will automatically resolve these as the Composite 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 Composite 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 Composite Component view definition, ccdemo-name-badge.html I could use a Knockout ifnot test to only display the first name data if the mode is not compact:
<div class="badge-face">
<img class="badge-image" data-bind="attr:{src: $props['badge-image'], alt: $props['badge-name']}"/>
<!-- ko ifnot : $props.compactView -->
<h2 data-bind="text: upperFirstName"/>
<!-- /ko -->
<h3 data-bind="text: $props['badge-name']"/>
</div>
Pattern 2 - Consolidated Template Based UIs
Leading on from the basic use of Knockout if, foreach etc. 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 Composite Component. To show this, I'll use the same Composite Component compactView boolean property as before. The big alteration is in the Composite 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" data-bind="attr:{src: $props['badge-image'], alt: $props['badge-name']}"/>
<h3 data-bind="text: $props['badge-name']"/>
</script>
<script type="text/html" id="fullTemplate">
<img class="badge-image" data-bind="attr:{src: $props['badge-image'], alt: $props['badge-name']}"/>
<h2 data-bind="text: upperFirstName"/>
<h3 data-bind="text: $props['badge-name']"/>
</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 Composite 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 Composite 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['badge-name'];
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: {inline: JSON.parse(metadata)},
viewModel: {inline: ComponentModel},
view: {inline: "<!-- ko template: {'nodes' : templateForView} --><!-- /ko -->"},
css: {inline: css}
});
}
);
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 Composite 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 Composite Component can use ojModule to present multiple views, each with their own viewModel.
CCA Series Index
- Introduction
- Your First Composite Component - A Tutorial
- Composite Conventions and Standards
- Attributes, Properties and Data
- Events
- Methods
- The Lifecycle
- Slotting Part 1
- Slotting Part 2
- Custom Property Parsing
- Metadata Extensibility
- Advanced Loader Scripts
- Deferred UI Loading