Introduction
In this article I'll be looking at both the built-in events supported by the Composite Component Architecture and the way that you can add in events of your own on top of that.
Build-In Events
Out of the box, every Composite Component that you create will support a set of pre-defined events:
- pending event
- ready event
- property-changed events
The pendingEvent
The pending event is raised to let consuming views know that a particular Composite Component is about to render. The event itself will include the identity of the Composite Component that raised the event in its target property.
The readyEvent
As a companion to pending, the Composite Component will also raise a ready event once it is fully rendered. Like the pending event, it will include the identity of the source Composite Component.
To show these two events in action I'll make a slight amendment to the viewModel of the workArea module that I've been using throughout this series to add an event handler for these events:
define(['ojs/ojcore', 'knockout', 'jquery','components/ccdemo-name-badge/component'],
function(oj, ko, $) {
function WorkAreaViewModel() {
var self = this;
self.personName = ko.observable('Duke Mascot');
self.personImageURL = ko.observable('/images/duke.png');
self.startupTimes = {};
self.badgeStartupMonitor = function(viewmodel, event) {
var targetCCA = event.target.id;
var timeStamp = new Date().getMilliseconds();
if (event.type === 'pending'){
self.startupTimes[targetCCA] = timeStamp;
}
else if (event.type === 'ready'){
var startupTime = (timeStamp - self.startupTimes[targetCCA]);
console.log('Startup time for '
+ targetCCA + ': ' + startupTime + 'ms');
}
};
}
return new WorkAreaViewModel();
}
);
What I've done here is add a new variable
startupTimes and a method called
badgeStartupMonitor to handle the two lifecycle events. This method is then bound to the events (in this case) using Knockout
1 in the view for the module (workArea.html):
<div>
<h2>Test Composite</h2>
<ccdemo-name-badge id="cc1"
badge-name="{{personName}}"
badge-image="[[personImageURL]]"
data-bind="event : {pending : badgeStartupMonitor,
ready : badgeStartupMonitor}"/>
</div>
The result will then be printed onto the console:
Startup time for cc1: 9ms
Of course, this use of the events is pretty trivial, but in general, the ability for the consuming view to understand when the Composite Component is
ready can be very useful for more complex components, particularly when the component may be having to contact external services before it is usable.
Property Changed Events
A really nice feature of the Composite Component Architecture is the automatic creation of property changed events for the properties that you defined in the component metadata. You don't have to do anything here in the component, to make this happen, it's automatic. The event is named after the name of the Composite property with -changed appended.
So again I could add an event handler to my consuming (workArea) viewModel to listen for a change on the badge-name property and register that:
<div>
<h2>Test Composite</h2>
<ccdemo-name-badge id="cc1"
badge-name="{{personName}}"
badge-image="[[personImageURL]]"
data-bind="event : {'badge-name-changed' : badgeNameChangeWatcher}"/>
<:/div>
Information with a PropertyChanged Event
Key to the event object that you will be passed for a Composite property change event is the detail attribute. This will encapsulate an object with three properties:
- previousValue - the value of the property before it was changed
- value - the new value for the property
- updatedFrom - will be set to either the value external or internal to indicate where the property change was instigated. If, for example, your property is bound through the tag attribute to an observable in the consuming view (using {{...}} syntax) and the consuming viewModel updates the observable, that change will be automatically propagated into the Composite property and the event raised with external as the updatedFrom value. Correspondingly, a change to the property from within the Composite Component will set the value as internal.
Property Changes within a Composite Component
We can also use the in-build property changed events within the component itself. This is mostly going to be useful in the binding case where the change to the property is triggered at some random point in time by an external (to the component) change. If we go back to our running example. In Part IV I added some code to extract the first name in upper case format from the inbound badge-name property. I mentioned in that article that because the lifecycle is not re-run when a property changes, then even if a bound observable changes, this uppercased first-name value would not get recomputed. Now that we have learnt about the property change listener, it becomes trivial to fix this.
So here's the revised version of the Composite Component viewModel. I've made the following changes:
- Extract the first-name generation process into its own function to allow re-use
- The upperFirstName value is now defined as an observable
- Registered a badge-name-changed listener within the composite which will call this new function
define(
['ojs/ojcore','knockout','jquery'
], function (oj, ko, $) {
'use strict';
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);
});
self.composite = context.element;
$(self.composite).on('badge-name-changed',function(event){
if (event.detail.updatedFrom === 'external'){
self._extractFirstName(event.detail.value);
}
});
};
CCDemoNameBadgeComponentModel.prototype._extractFirstName
= function (fullName) {
if (this.upperFirstName === undefined){
this.upperFirstName = ko.observable();
}
this.upperFirstName(fullName.split('')[0].toUpperCase());
};
return CCDemoNameBadgeComponentModel;
});
You should notice from this code that we are obtaining the reference to the Composite Component element from the
context object that is passed into the constructor. This is the same object that carries the property promise.
One additional point on lifecycle here. Notice that I'm still calling the _extractFirstName from the constructor as well as from the property change listener. This is because the initial setting of the property value from the tag attribute does not raise the property change events, these are only emitted after the component is ready.
Custom Events
Useful as the build-in events are, complex custom components may need to specify a more complex and function-orientated API for its consumers to use. For example, a Composite Component that provides a chat capability may need to omit an event to signal an incoming message, allowing the consuming view to react to this in some way.
The Composite Component Architecture provides for such custom events in the metadata that you define for the composite.
Looking at our ongoing example of the ccdemo-name-badge component we currently only define properties in the metadata JSON file (ccdemo-name-badge.json). As a peer of the existing properties definition within that object structure we can define events as well. So as an example, I'll add an event called badgeSelected to the Component. Here's what the metadata will now look like:
{
"properties": {
"badge-name": {
"description" : "Full name to display on the badge",
"type": "string"
},
"badge-image": {
"description" : "URL for the avatar to use for this badge",
"type": "string"
}
},
"events" : {
"badgeSelected" : {
"description" : "The event that consuming views can use to recognize when this badge is selected",
"bubbles" : true,
"cancelable" : false,
"detail" : {
"nameOnBadge" : {"type" : "string"}
}
}
}
}
This is all pretty self explanatory in terms of the attributes that define the event. The
detail sub-object definition may need a little explanation though. This is simply so that you can declare an specific information that you will be passing back within the detail property of the event. In this case, I'll be putting the current badge-name value into the detail, referenced as
nameOnBadge.
A key point to stress at this stage is that adding an events section to the metadata in this way does not actually do anything at runtime. It simply provides documentation to the consumer and possibly design time tooling about what events are emitted and what the properties of those events are. In fact, it is the responsibility of you, the Composite Component author to actually create and raise the event, in doing so, you should be careful that it matches the API that you have declared in the metadata.
Raising a Custom Composite Component Event
To actually raise an event you will need to use the dispatchEvent function. In this revised version of the Composite Component viewModel you can see this done in the _raiseBadgeSelection function which is triggered by a click or enter key on the composite.
define(
['ojs/ojcore','knockout','jquery'
], function (oj, ko, $) {
'use strict';
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);
});
self.composite = context.element;
$(self.composite).on('badge-name-changed',function(event){
if (event.detail.updatedFrom === 'external'){
self._extractFirstName(event.detail.value);
}
});
//Wire the custom event raise function into the click on
//the composite
$(self.composite).on('click keypress',function(event){
self._raiseBadgeSelection(event);
});
};
CCDemoNameBadgeComponentModel.prototype._extractFirstName
= function (fullName) {
if (this.upperFirstName === undefined){
this.upperFirstName = ko.observable();
}
this.upperFirstName(fullName.split('')[0].toUpperCase());
};
//Generate and raise the custom event for Badge Selection
CCDemoNameBadgeComponentModel.prototype._raiseBadgeSelection
= function (sourceEvent) {
if (sourceEvent.type === 'click' ||
(sourceEvent.type === 'keypress'&& sourceEvent.keycode === 13)){
var eventParams = {
'bubbles' : true,
'cancelable' : false,
'detail' : {
'nameOnBadge' : this.properties['badge-name']
}
};
//Raise the custom event
this.composite.dispatchEvent(new CustomEvent('badgeSelected',
eventParams));
}
};
return CCDemoNameBadgeComponentModel;
});
Notice how the custom event is created with parameters which match those declared in the metadata. The listening code can therefore inspect
event.detail.nameOnBadge to identify the selected badge when the event is received.
What Next?
Now that both properties and events have been covered the next article in the series will complete the component picture by looking at methods.
Composite Component Article Series Index
- Introduction
- Your First Composite Component - A Tutorial
- Composite Conventions and Standards
- Attributes, Properties and Data
- Events