X

Technical Articles relating to Oracle Development Tools and Frameworks

  • JET
    January 30, 2017

JET Custom Components V - Events

Duncan Mills
Architect

Introduction

In this article I'll be looking at both the built-in events supported by the Custom JET 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 custom component that you create will support a set of pre-defined events that are just normal DOM events that your application can listen to:

  • Property Changed events
  • pending event (deprecated)
  • ready event (deprecated)

Property Changed Events

A really nice feature of the 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 component property with Changed appended. 
So I could add an event handler to my consuming (workArea) viewModel to listen for a change on the badgeName property.
Note that I have to follow the same camel case to hyphen conversion that we use for property to tag attribute name mapping to account for the case insensitive nature of the HTML tag.  So although the actual event raised is called (in this case) badgeNameChanged, the attribute to wire up a listener is on-badge-name-changed.


<div>
    <h2>Test Composite</h2>
    <ccdemo-name-badge id="cc1"
    badge-name="{{personName}}"
    badge-image="[[personImageURL]]"
    on-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 component property and the event raised with external as the updatedFrom value. Correspondingly, a change to the property from within the component  will set the value as internal.

Note that property change events do not bubble and so you must add the handler directly to the custom component or more usually you actually register the handler inside of the component itself. Let's look at that next.

Property Changes within a Custom Component

As mentioned, 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 component viewModel. I've made the following changes:

  1. Extract the first-name generation process into its own function to allow re-use
  2. The upperFirstName value is now defined as an observable
  3. Registered a listener for the  badgeNameChanged event within the composite which will call this new function. I've put this listener into the attached lifecycle method which is generally where you will need to do such things. (more on this in a later article)
 

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.badgeName;
            self._extractFirstName(badgeNameAttr);
        });
    };

     CCDemoNameBadgeComponentModel.prototype.attached = function(context) {
        self.composite = context.element;
        self.composite.addEventListener('badgeNameChanged',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 component element from the context object that is passed into the attached() lifecycle method. This is the same object that carries the property promise into the constructor.

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.

Deprecated events

The following two events have been deprecated by the component framework and should no longer be used in JET 4.0 or above. Instead the BusyContext is used to handle this, something which I'll talk about in a later article.

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.

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 Custom JET 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 JET Component Architecture provides for such custom events in the metadata that you define for the component. These are standard DOM custom events.

Looking at our ongoing example of the ccdemo-name-badge component we currently only define properties in the metadata JSON file (component.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:


{
  "name" : "ccdemo-name-badge",
  "version" : "1.0.0",
  "jetVersion" : "^4.2.0",
  "properties": {
    "badgeName": {
      "description" : "Full name to display on the badge",
      "type": "string"
    },
    "badgeImage": {
      "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 badgeName 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 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 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 Component Event

To actually raise an event you will need to use the dispatchEvent function. In this revised version of the 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.badgeName;
            self._extractFirstName(badgeNameAttr);
        });
    };

    CCDemoNameBadgeComponentModel.prototype.attached = function(context) {
        self.composite = context.element;
        self.composite.addEventListener('badgeNameChanged',function(event){
            if (event.detail.updatedFrom === 'external'){
                self._extractFirstName(event.detail.value);
            }
        });
        //Wire the custom event raise function into the click
        //and return key on the component
        self.composite.addEventListener('click',function(event){
            self._raiseBadgeSelection(event);
        });
        self.composite.addEventListener('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.badgeName
                }
            };
            //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.


JET Custom Component Series

If you've just started with 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.