X

Technical Articles relating to Oracle Development Tools and Frameworks

  • JET |
    August 14, 2017

JET Custom Components XX - Working With Properties Safely

Duncan Mills
Architect

Introduction

In this article I wanted to re-visit one of the most basic features of Composite Components, that of properties. Specifically I want to talk about properties in the context of the lifecycle. If you've been following along with this series of articles you may be thinking, hold on, we know all about properties, that was all covered way back in the first few of the series - it's about as basic as you can get right? And you'd be correct, however, there are a couple of traps which are easy to fall into and can lurk as a time-bomb in your code just waiting to catch you out. It's all, as they say, in the timing...

An Illustration of the First Problem

Let's look at this with a simple bit of code, you'll recognise some of this from other articles, it's meaningless but illustrates the point:

define(

  ['ojs/ojcore', 'knockout', 'jquery'], 
  function (oj, ko, $) {
  'use strict'
  /*
   * Component constructor
   */
  function CoyoteTrackerComponentModel(context) {
    var self = this;
    self.isLoonyTunes = false;
    //Parse out and store the properties
    context.props.then(function (propertyMap) {
       //store the properties for easy reference
      self.properties = propertyMap;
      
      //Check for a certain coyote
      if (propertyMap.coyoteName !== undefined &&
          propertyMap.coyoteName === 'Wile E. Coyote' ) {
        self.isLoonyTunes = true;
      }
    });
  };
  /*
   * Attached Lifecycle method
   */
   CoyoteTrackerComponentModel.prototype.attached = function(context) {
     //get the length of the coyote name 
     var nameLen = self.properties.coyoteName.length;
   };
// And so on 

So what's wrong?

That fragment of code seems reasonable right? Well no, and the problem comes down to the fact that the properties supplied to the component are made available in the form of a promise (props) on the context. The point about this promise is that you can't make the assumption that it will have been resolved by any particular stage of the lifecycle. I've seen this catch several developers out because of course it's not consistent. It may well be that throughout all your testing the promise has resolved in time for the self.properties value to have been set by the time that the attached (or other) phase in the lifecycle executes. However, one day it won't!

And the Solution?

There are a couple of basic rules that you can follow to ensure that your code is saved from this particular trap:

  1. Within your (startup) lifecycle methods, only reference properties from within the resolved promise (e.g. the then clause. The methods all have the same context passed into them that contains the props promise and it will do no harm to have multiple context.props.then() statements across the lifecycle.
  2. Outside of the lifecycle methods you should code a little defensively whenever referencing a stored reference to properties. It is possible that some of your methods (especially event handlers) might be called before the promise has resolved

Just being aware of this potential timing issue is the battle here, once you have your head around the implications of this on your component startup, the rest is easy. Here's the fixed attached method


  /*
   * Attached Lifecycle method
   */
   CoyoteTrackerComponentModel.prototype.attached = function(context) {
    context.props.then(function (propertyMap) {
      //get the length of the coyote name *safely*
      var nameLen = self.properties.coyoteName.length;
     });
   };

The Second Problem - External Property Changes

The second timing issue as a little more subtle and frankly I doubt that most developers are likely to be effected by it. However, it can lead to similarly hard to diagnose problems and fore-warned is fore-armed.

The scenario here relates to a component when some parts of your component view are bound directly to the properties observables via the $props root. e.g.

<!-- ko ifnot:$props.acmeInvsibilityCloak -->
  <input data-bind="ojComponent : 
    {component: 'ojInputText',
     value: coyoteName}">
<!-- /ko -->

And you are also listening to external changes to that same property via the implicit acmeInvsibilityCloakChanged event. The question in this case is what happens first?  Does the acmeInvsibilityCloakChanged event execute before the knockout "ifnot" evaluation is completed or visa-versa?   In most cases it frankly wouldn't matter, but it is possible that whatever you are doing inside of your event handler is depending upon some side effect of the $props.acmeInvsibilityCloak update having happened, as in this case, we might want to inspect the field within that if block and so we'll need to wait until it is there.

So What's the Ordering? 

So in fact, if a property is changed by some external agency (e.g. the event source is external) then your *Changed event will be delivered before the $props observable is updated. So it is possible that, should you inspect the state of any UI component in your view, you will be seeing the component view in a pre-update state. This is where problems can creep in.

The real solution to this particular conundrum is to re-think the problem a little.  In this case, the code that you want to execute is dependent on something happening in the UI in response to the property changing.  So, rather that having code in the property *Changed listener why not wait for the actual change that you are relying on to take place. In many cases the optionChange event on any components that you are interested will fire when the change has had a chance to take effect. 

In this particular example, however, we can't use an optionChange listener directly on the component because it won't exist to attach the listener to until the knockout ifnot statement is evaluated, so i's a bit of a Catch-22 situation.

We can, however, take an alternative approach which is to handle the reactive action in  when the promise returned by the JET BusyContext whenReady() function resolves which you can define in the *Changed handler. It will then get processed asynchronously after the $props observable has been updated:

 $(self.composite).on('acmeInvisibilityCloakChanged', function (event) {
  if (event.detail.updatedFrom === 'external' && 
      event.detail.value = false) {
    //self.composite is a reference to the CCA element obtained from the context
    var busyContext = oj.Context.getContext(self.composite).getBusyContext();
    busyContext.whenReady().then( function(){
      //only validate once the UI has been updated to make the coyoteInput visible 
      var coyoteFieldValid = self.coyoteInput.ojInputText('validate');
      ...
    });
  }
});

Bear in mind though, that it's always better to observe the results of the property change and then act on that, rather than trying to react to the property change event itself.


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 Architecture 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.Captcha
Oracle

Integrated Cloud Applications & Platform Services