Showing posts with label Promise. Show all posts
Showing posts with label Promise. Show all posts

Saturday, 22 August 2020

App Builder Page Aware Lightning Web Component

Introduction

This week I've been experimenting with decoupled Lightning Web Components in app builder pages, now that we have the Lightning Message Service to allow them to communicate with each other without being nested. 

A number of the components are intended for use in record home and app pages, which can present challenges, in my case around initialisation. Some of my components need to initialise by making a callout to the server, but if they are part of a record home page then I want to wait until the record id has been supplied. Ideally I want my component to know what type of page it is currently being displayed in and take appropriate action.

Inspecting the URL

One way to achieve this is to inspect the URL of the current page, but this is a pretty brittle approach as if Salesforce change the URL scheme then it stands a good chance of breaking. I could use the navigation mixin to generate URLs from page references and compare those with the current URL, but that seems a little clunky and adds a delay while I wait for the promises to resolve.

targetConfig Properties

The solution I found with the least impact was to use targetConfig stanzas in the component's js-meta.xml configuration file. From the docs, these allow you to:

Configure the component for different page types and define component properties. For example, a component could have different properties on a record home page than on the Salesforce Home page or on an app page.

It was this paragraph that gave me the clue - different properties depending on the page type!

You can define the same property across multiple page types, but define different default values depending on the specific page type. In my case, I define a pageType property and default to the type of page that I am targeting:

<targetConfigs>
    <targetConfig targets="lightning__RecordPage">
        <property label="pageType" name="pageType" type="String" default="record" required="true"/>
    </targetConfig>
    <targetConfig targets="lightning__AppPage">
        <property label="pageType" name="pageType" type="String" default="app" required="true"/>
    </targetConfig>
    <targetConfig targets="lightning__HomePage">
        <property label="pageType" name="pageType" type="String" default="home" required="true"/>
    </targetConfig>
</targetConfigs>

so for a record page, the pageType property is set as 'record' and so on.

In my component, I expose page type as a public property with getter and setter methods (you only need the @api decorator on one of the methods, and convention right now seems to be the getter) :

@api get pageType() {
    return this._pageType;
}

set pageType(value) {
    this._pageType=value;
    this.details+='I am in a(n) ' + this._pageType + ' page\n';
    switch (this._pageType) {
        case 'record' :
             this.details+='Initialisation will happen when the record id  is set (which may already have happened)\n';
             break;
            ;;
        case 'app' :
        case 'home' :
            this.details+='Initialising\n';
            break;
    }                
 }

and similar for the record id, so that I can take some action when that is set:

@api get recordId() {
    return this._recordId;
}

set recordId(value) {
    this._recordId=value;
    this.details+='I have received record id ' + this._recordId + ' - initialising\n';
}

then I can add the component to the record page for Accounts:



a custom app page:



and the home page for the sales standard application:

and in each case the component knows what type of page it has been added to. 

Of course this isn't foolproof - my Evil Co-Worker could edit the pages and change the values in the app builder, leading to all manner of hilarity as my components wait forlornly for the record id that never comes. I could probably extend my Org Documentor to process the flexipage metadata and check the values haven't been changed, but in reality this is fairly low impact sabotage and probably better that the Evil Co-Worker focuses on this rather than something more damaging.

Show Me The Code!

You can find the code at the Github repo.

Related Posts


Friday, 30 December 2016

JavaScript Promises in Lightning Components

JavaScript Promises in Lightning Components

Promise

Introduction

Promises have been around for a few years now, originally in libraries or polyfills but now natively in JavaScript for most modern browsers (excluding IE11, as usual!).

The Mozilla Developer Network provides a succinct definition of JavaScript promises:

A Promise is a proxy for a value not necessarily known when the promise is created

Promises can be in one of three states:

  • pending - not fulfilled or rejected (Heisenberg, for Breaking Bad fans!)
  • fulfilled  - the asynchronous code successfully completed
  • rejected - an error occurred executing the asynchronous code

For me, the key advantage with Promises is that they allow asynchronous JavaScript code to be written in a way that looks somewhat like synchronous code, and is thus easier for someone new to the implementation to understand.

Creating a Promise

var promise = new Promise(function(resolve, reject) {

  // asynchronous code goes here

  if (success) {
     /* everything worked as expected */
     resolve("Excellent :)");
  }
  else {
    /* something went wrong */
    reject(Error("Bogus :("));
  }
});
 

The Promise constructor takes a callback function as a parameter. This callback function contains the asynchronous code to be executed (to retrieve a record from the Salesforce server, for example). The callback function in turn takes two parameters that are also functions - note that you don’t have to write these functions, just invoke them based on the outcome of the asynchronous code:

  • resolve - this function is invoked if the asynchronous code executes successfully. Executing this function moves the Promise to the fulfilled state
  • reject - this function is invoked with an Error if anything goes wrong in the asynchronous code. Executing this function moves the Promise to the rejected state.

Handling the Result

Thus far all well and good, but pretty much all of the asynchronous code that I write is carrying out a remote activity and returning the result, so I need some way to be notified when the asynchronous code has completed. Enter the Promise.then() function:

promise.then(function(data) {
                alert('Success : ' + data);
             },
             function(error) {
                alert('Failure : ' + error.message);
             });

Promise.then() takes two functions as parameters - the first is a success callback, invoked if and when the promise is resolved, and the second is an error callback, invoked if and when the promise is rejected.

A Real World Example

When building Lightning Components, asynchronous interaction with the Salesforce server is typically carried out via an action. The following function takes an action and creates a Promise around it:

executeAction: function(cmp, action, callback) {
    return new Promise(function(resolve, reject) {
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === "SUCCESS") {
                var retVal=response.getReturnValue();
                resolve(retVal);
            }
            else if (state === "ERROR") {
                var errors = response.getError();
                if (errors) {
                    if (errors[0] && errors[0].message) {
                        reject(Error("Error message: " + errors[0].message));
                    }
                }
                else {
                    reject(Error("Unknown error"));
                }
            }
        });
	$A.enqueueAction(action);
    });
}

the executeAction function instantiates a Promise that defines the action callback handler and enqueues the action. When the action completes, the callback handler determines whether to fulfil or reject the Promise based on the state of the response.

This function can then be used to create a Promise to retrieve an account: 

var accAction = cmp.get("c.GetAccount");
var params={"accountIdStr":accId};
accAction.setParams(params);
        
var accountPromise = this.executeAction(cmp, accAction);

and callback handlers provided to process the results:

accountPromise.then(
        $A.getCallback(function(result){
            // We have the account - set the attribute
            cmp.set('v.account', result);
        }),
        $A.getCallback(function(error){
            // Something went wrong
            alert('An error occurred getting the account : ' + error.message);
        })
     );

Note that the success and error callbacks are encapsulated in $A.getCallback functions as they are executed asynchronously, and therefore are outside of the Lightning Components lifecycle. Note also that if you forget to do this, quite a lot of the promise functionality will still work, which will make it difficult to track down what the exact problem is!

Chaining Promises

The Promise.then() function can return another Promise, thus setting up a chain of asynchronous operations that each complete in turn before the next one can start. Repurposing the above example to retrieve a Contact from the Account:

accountPromise.then(
        $A.getCallback(function(result){
            // We have the account - set the attribute
            cmp.set('v.account', result);

            // return a promise to retrieve a contact
            var contAction = cmp.get("c.GetContact");
            var contParams={"accountIdStr":accId};
            contAction.setParams(contParams);
            var contPromise=self.executeAction(cmp, contAction);
            return contPromise;
        }),
        $A.getCallback(function(error){
            // Something went wrong
            alert('An error occurred getting the account : ' + error.message);
        })
   )
   .then(
        $A.getCallback(function(result){
            // We have the contact - set the attribute
            cmp.set('v.contact', result);
        }),
        $A.getCallback(function(error){
            // Something went wrong
            alert('An error occurred getting the contact : ' + error.message);
        })
     );
    

However there is a side effect here - the second then() is executed regardless of the success/failure of the first. If the first Promise was rejected, the success callback for the second then() is executed with a null value. While this would be benign in the above example, it’s probably not behaviour that is desired in most cases. What would be better is that the second then() is only executed if the first one is successful. Enter the Promise.catch() function.

Catching Errors

The Promise.catch() function is invoked if a Promise is rejected, but the then() function didn’t provide an error callback:

promise.then(function(data) {
                alert('Success : ' + data);
             })
        .catch(function(error) {
                alert('Failure : ' + error.message);
             });

When chaining Promises, the catch() function becomes more powerful, as if a Promise is rejected and the then() function did not provide an error callback, control moves forward to either the next then() function that does provide an error callback, or the next catch() function.

Refactoring the example again:

accountPromise.then(
        $A.getCallback(function(result){
            // We have the account - set the attribute
            cmp.set('v.account', result);

            // return a promise to retrieve a contact
            var contAction = cmp.get("c.GetContact");
            var contParams={"accountIdStr":accId};
            contAction.setParams(contParams);
            var contPromise=self.executeAction(cmp, contAction);
            return contPromise;
        })
   )
   .then(
        $A.getCallback(function(result){
            // We have the contact - set the attribute
            cmp.set('v.contact', result);
        })
   .catch(
        $A.getCallback(function(error){
            // Something went wrong
            alert('An error occurred : ' + e.message);
        })
     ); 

The second then() function is now only executed if the account retrieval is successful. An error occurring retrieving either the account or the contact immediately jumps forward to the catch() function to surface the error.

Going back to the original point made in the introduction, I now have two asynchronous operations, with the second dependent on the success of the first, but coded in a readable fashion.

Further Reading

Promises are a tricky one to wrap your head around, and its certainly worth spending some time learning the basics and playing around with examples. I've found the following resources very useful:

Related Posts