X

Technical Articles relating to Oracle Development Tools and Frameworks

  • January 21, 2019

Web Component Techniques - Finesse the Component Lifecycle

Duncan Mills
Architect

Introduction

In this occasional series of Web Component Techniques, I discuss common patterns and topics that Oracle JET Custom Web Component (CCA) authors are likely to need at one point or another.  In this article I'll be looking at how the component  is not quite as fixed in stone as it appears and the key technique that you can use to bend it to your will. 

The "Problem" - Back to Basics

To summarise the issue that you might be confronting let's just quickly re-visit the basic component initialisation lifecycle as implemented in JET:

  1. Component constructor - runs just the once of course and before the component is attached to the DOM
  2. activated - again fires just once, after the constructor and allows you to "hold" the initialisation of the component be returning a promise, in which case the component lifecycle will not move forward until that promise is resolved or rejected.  This is useful if your component needs to do some asynchronous setup before you can show anything, for example make a fetch from a REST endpoint
  3. connected - may fire multiple times over the lifespan of the component as it is attached / re-attached to the DOM, something that might happen if the component (for example) was in a popup.
  4. bindingsApplied - fires just once at the point that all of the internal components being used by the custom web component have been upgraded and are fully functional

Component authors have to decide how best to use this sequence in terms of where to put certain functions.  For example, as discussed above, if the component absolutely must have data before it can be displayed then that code belongs in activated.  Likewise, if your code needs to do something like getElementById on one of it's subcomponents, then that has to be done in the bindingsApplied, because that's when any :id references in the component view will have been resolved.  Trying to find a component by ID earlier in the lifecycle will simply not work.

So this illustrates a particular problem - what if you need to carry out some action in a phase of the lifecycle (let's pick on connected) that is dependent on a later phase in some way, e.g. bindingsApplied. This seems to be an intractable problem because the former executes after the latter?   This is where this pattern comes into play. 

Promises Promises

So here's how we deal with it.  You need an approach that will work both on the first run-though if the lifecycle and subsequent invocations such as call-backs to the connected method over the lifecycle of the component.  We achieve this using a promise which can be used to "hold" execution of a piece of code until all of the required parameters are met, something that will work no matter if this is the first run through the lifecycle or if this is a reconnect. So working with our example of connected requiring an element reference that won't be available until bindingsApplied.... 

Step 1 - Defer the code execution in connected()

In our connected method we enclose the code that might need to be executed in a deferred manner in a then() block as follows


MyComponentModel.prototype.connected(context){
  var self = this;
  self.getInstanceSemaphore().then(function(){
     // Do lifecycle critical stuff here
     // e.g. getElementById calls
  });
}

As you will hopefully have gathered, the getInstanceSemaphore() function will be returning a promise and it when that promise resolves that the code can be executed

Step 2 - Implement the getInstanceSemaphore()

The getInstanceSemaphore() method is pretty generic, all it is really doing is just creating a marker promise that can be resolved at any point in the lifecycle that you choose. Note how it saves the resolve callback to the component model (via self) so that some other phase can resolve the promise and allow pending actions to take place


MyComponentModel.prototype.getInstanceSemaphore = function () {
  var self = this;
  if (self.semaphorePromise) {
    return self.semaphorePromise;
  }
  else {
    self.semaphorePromise = new Promise(function (resolve) {
      self.instanceSemaphoreResolver = resolve;
    });
    return self.semaphorePromise;
  }
}

Step 3 - Signal ready state in bindingsApplied()

All that remains is for some piece of code to call the stored resolver for the semaphore promise. This would typically be in bindingsApplied as this is at the very end of the initialisation lifecycle and things such as dynamic IDs will have been evaluated and applied. 


MyComponentModel.prototype.bindingsApplied(context){
  var self = this;
  //Any normal code for this phase
  ...
  //And finally ready to resolve all the waiting code
  self.instanceSemaphoreResolver();
  //clean up the resolver, we don't need it any more
  delete self.instanceSemaphoreResolver;
}

So, the use of this semaphore mechanism will mean that the very first time that the connected() method executes, the critical code will sit and wait for bindngsApplied() to have been run before it kicks off. However, on subsequent re-connects, the promise will have already have been resolved and the code can execute immediately, giving you the best of both worlds.


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