Pages

Thursday, May 5, 2022

Understanding of Async/Await in LWC - Extension to Promise




In the last post, we discussed about Promise function How to chain Asynchronous actions in LWC using Promise? While they provide easy control to Asynchronous JavaScript, but Async/Await which builds on Promises allowing them to be used in a way that much more closely resembles synchronous JavaScript.

Async

It is simply a keyword we put in front of a function definition. Async functions are instances of the AsyncFunction constructor.

async function performAction() {
   // do some asynchronous action
   return 'result of an action';
}

An Async function always returns a Promise. That promise will contain the value that you defined.

If you look at the below code script

function start() {
    console.log('start');
    console.log(performAction());
    console.log('stop');
}

start();

You might expect the below result

start
result of an action
stop

But in reality the result will be 

start
stop
result of an action

This is because performAction() was executed asynchronously.

Now, to make the above code work like synchronously or to execute all calls in sequential order we need to use Await function.

Await

The await expression causes the async function to pause until a Promise is returned for the async function. So, in simple words we are essentially telling our code to wait for the result of the asynchronous function to complete going any further.

await always expects a Promise from the async function, and parses the result automatically.

Lets consider the above code snippet

async function performAction() {
   // do some asynchronous action
   return 'result of an action';
}

async function start() {
    console.log('start');
    console.log(await performAction());
    console.log('stop');
}

start();

Now the result will be

start
result of an action
stop

This is because we are waiting the function performAction() to be completed before proceeding further.

NOTE: await can only be used within Async function.

Usage in LWC

Problem Statement : I need to re-render the validation message whenever the user checks Another amount as a radio button, but for me the DOM is not getting rendered properly on UI. So, I need to make a wait before I check for the selected radio button and checks the validity and reset it. So, to make that happen I need to put my change action in async/await.

Below is the code snippet:

payment.html

<!--
  @description       : 
  @author            : @tandonprateek
  @group             : 
  @last modified on  : 04-15-2022
  @last modified by  : @tandonprateek
-->
<template>
	<div class="slds-grid slds-gutters slds-var-m-bottom_small">
		<div class="slds-col slds-size_1-of-3 slds-nowrap">
			{labels.component.amountToPay}
		</div>
		<div class="slds-col">
			<lightning-radio-group 
                name="amountToPay"
                label={labels.component.amountToPay} 
                variant="label-hidden"
				options={amountToPayOptions} 
                value={amountToPaySelection} 
                type="radio" 
                onclick={handleAmountToPayChange}
				onchange={handleAmountToPayChange} 
                class="slds-var-m-bottom_small requiredInput" required>
			</lightning-radio-group>
			<lightning-input 
                type="number" 
                name="anotherAmount" 
                label="Another Amount" 
                variant="label-hidden"
				value={anotherAmount} 
                min="1" onfocus={handleAnotherAmountFocus} 
                onchange={handleAnotherAmountChange}
				formatter="currency" 
                class="inputAmount anotherAmount requiredInput" required={isAnotherAmountRequired}>
			</lightning-input>
		</div>
	</div>
</template>

payment.js

/**
 * @description       : 
 * @author            : @tandonprateek
 * @group             : 
 * @last modified on  : 05-05-2022
 * @last modified by  : @tandonprateek
**/
import { LightningElement } from 'lwc';

const ANOTHER_AMOUNT_OPTION = 'anotherAmount';
const AMOUNT_DUE_TODAY_OPTION = 'amountDueToday';


export default class Payment extends LightningElement {

    labels = {
        toast: {
            processingErrorTitle: 'Processing Error'
            , validationErrorTitle: 'Input Validation Error'
            , validationErrorMessage: 'Please provide values for all required fields'
        },
        component: {
            paymentHeader: 'Payment'
            , amountToPay: 'Amount to Pay'
            , amountDueToday: 'Amount Due Today '
            , remainingBalance: 'Remaining Balance '
            , anotherAmount: 'Another Amount'
        }
    }

    amountToPaySelection;
    anotherAmount;
    isAnotherAmountRequired = false;

    connectedCallback() {
        this.amountToPaySelection = AMOUNT_DUE_TODAY_OPTION
    }

    /**
        This method will wait for the Promise to be resolved and will then reset the validity.
    **/
    async handleAmountToPayChange(event) {
        this.amountToPaySelection = event.detail.value;
        const anotherAmountField = this.template.querySelector('.anotherAmount');
        if(this.amountToPaySelection !== ANOTHER_AMOUNT_OPTION) {
            this.anotherAmount = undefined;
            await Promise.resolve();
            anotherAmountField.reportValidity();
        } else {
          anotherAmountField.focus();
          anotherAmountField.reportValidity();
        }
        this.isAnotherAmountRequired = this.amountToPaySelection === ANOTHER_AMOUNT_OPTION;
    }
    
    handleAnotherAmountChange(event) {
        const value = event.detail.value;
        this.anotherAmount = value;
    }
    
    handleAnotherAmountFocus(event) {
        this.amountToPaySelection = ANOTHER_AMOUNT_OPTION;
        this.isAnotherAmountRequired = this.amountToPaySelection === ANOTHER_AMOUNT_OPTION;
    }
    
    get amountToPayOptions() {
        const theOptions = [];
        theOptions.push({ label: this.labels.component.amountDueToday, value: AMOUNT_DUE_TODAY_OPTION});
    
        let balanceLabel = this.labels.component.remainingBalance;
    
        theOptions.push({ label: balanceLabel, value: 'remainingBalance'});
        
        theOptions.push({ label: this.labels.component.anotherAmount, value: ANOTHER_AMOUNT_OPTION });
    
        return theOptions;
    }

}




Error Handling 

To handle error we will be using old try/catch block.

async function doSomething() {
   // do something asyncronous
   return 'something';
}

async function start() {
    try {
        console.log('start');
        console.log(await performAction());
        console.log('end');
    } catch(e) {
        console.error(e);
    } finally {
        console.log('Do something no matter what');
    }
}

start();


Tuesday, April 19, 2022

How to chain Asynchronous actions in LWC using Promise?



As we are aware that Javascript can interpret only one statement at a time. With this, Javascript also doesn't face concurrency issue like other language faces while executing threads. Having said that, in Javascript, you can execute "Asynchronous Methods". These Asynchronous Methods are "Callbacks, Promises, Async/Await".


First, let's understand the difference between Callbacks and Promises before using Asynchronous actions in LWC.


Callbacks:


A callback is a function that is to be executed after another function has finished executing. Async callbacks are functions that are passed as arguments. and when that function is called it will start executing code in the background. When the background code finishes running, it calls the callback function to let you know the work is done. We use these callbacks because we want to avoid executing things out of order. If we want to wait for something in JavaScript, we need to use a callback.


Let's make Pizza from scratch using callbacks:


Synchronous Code:


 const makePizza = () => {

  const pizzaBase = makePizzaBase();

  const veggies = chopVeggies();

  const spreads = makePizzaSpreads();

  const cheese = grateCheese();

  const pizza = preparePizza(pb, veg, spread, cheese);

  return pizza;

 }

 makePizzaBase() = () => {

  //do something

 }


This synchronous makePizza() function runs in order, one function after another but what if we had a function that needed to be run first and other functions couldn't be running until after this function finishes. Let’s think of making pizza from scratch, you can’t put the veggies, spreads, and cheese on the pizza base until it’s made so you must wait until it's done. With synchronous code, it doesn’t wait it just does it. So, to fix this issue we have to Async callbacks.


Asynchronous Code:


const makePizza = () => {

  makePizzaBase(function(){

   chopVeggies(function(..arguments){

    makePizzaSpreads(function(arguments){

     grateCheese(function(arguments){

      preparePizza(){

       //pizza is ready

      }

     });

    });

   });

  });


Let’s consider the above asynchronous code which has a lot of code to execute. This code execution can cause an issue because we can have enough nested callbacks inside one another. This leads to what we call callback hell. Callback hell can puzzle the code with bugs that are illusory. For this reason, we need a way to get rid of so many nested callbacks.


Promises:


A promise is a returned object where you attach callbacks, instead of passing callbacks into a function. You can attach the callback after the successful completion of a task is called, .then(). inside this, you can pass a callback. Promises allow us to avoid callback hell by chaining multiple .then() on each other which avoids nested callbacks and a cleaner code. For the failure of completing a task, you can pass it through a .catch() block.


Let’s convert the above asynchronous callback code to Promise:


const makePizza = () => {

  return makePizzaBase()

   .then(veggies = > chopVeggies(vegetables))

   .then(pizzaSpreads => makePizzaSpreads(sauces))

   .then(cheese => grateCheese(cheese))

   .then(pizza => constructPizza());

  catch((bad pizza));

 }


In the above code, we turned our code into Promise. First, we will make the pizza base and on completion, we are returning the promise that will be passed to the next callback. Next, we did chaining on other functions that will execute one after the other in order by using .then(), making an async function. As we have observed that code looks cleaner and we avoided callback hell.


How we can use Promise in LWC:


A promise can have three stages

- Pending - Default state, not fulfilled, not rejected.

- Fulfilled - Operation completed.

- Rejected - Operation failed.


Syntax :


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

     //do something

});

 

Example:


promiseExample.html


<!--
  @description       : 
  @author            : @tandonprateek
  @group             : 
  @last modified on  : 04-19-2022
  @last modified by  : @tandonprateek
-->
<template>
    <lightning-input type="text" label="Input1" value={val1} onchange={val1Change}></lightning-input>
    <lightning-input type="text" label="Input2" value={val2} onchange={val2Change}></lightning-input>
    <br/>
    <lightning-button variant="brand" label="Demo Promise Example" title="" onclick={executePromise} class="slds-m-left_x-small"></lightning-button>
    <div>Promise status : {status}</div>
</template>


promiseExample.js


/**
 * @description       : 
 * @author            : @tandonprateek
 * @group             : 
 * @last modified on  : 04-19-2022
 * @last modified by  : @tandonprateek
**/
import { LightningElement } from 'lwc';

export default class PromiseExample extends LightningElement {
    val1; 
    val2;
    status;

    val1Change(event) {
        this.val1= event.target.value;
    }

    val2Change(event) {
        this.val2= event.target.value;
    }
    
  promiseDemoExample(arg1, arg2) {
    return new Promise((resolve, reject) => {
      if (arg1 == arg2) resolve();
      else reject();
    });
  }

  executePromise() {
    this.promiseDemoExample(this.val1, this.val2)
      .then((result) => {
        this.status = "Resolved and Fulfilled";
      })
      .catch((result) => {
        this.status = "Rejected and Failed";
      });
  }
}


The promiseDemoExample method returns a Promise which will resolve in the future. When we call the promiseDemoExample method with 2 parameters, it will check if they are equal, if they are equal promise will resolve, or else it will reject the promise.


In executePromise method when a promise is resolved or rejected it will call either .then() method (when resolved) or .catch() method (when rejected). 


Every time a promise is resolved it will go in .then() block and when a promise is rejected it will go into .catch() block. Along with it, you can also create .finally() block which will execute no matter what happens to promise.


In .then() or .catch() method you can chain other promise as well. Using chaining, you can execute the asynchronous method in sequential order.


For Example:


executePromise() {
    this.promiseDemoExample(this.val1, this.val2)
      .then((result) => {
        //do something or add new promise
      })
      .then((result) => {
        //do something or add new promise
      })
      .catch((result) => {
        //rejected
      });
  }


In the next post, I will discuss the Async/Await methods, as these methods are extensions of Promises. So stay tuned till then.




Thursday, April 14, 2022

Dynamic Tree Structure with LWC and Apex

In this post, I am going to share a component that I built while playing with LWC(Lightning Web Components). This is a simple LWC component with most logic handled by Apex.

While playing, I decided to create a sample tree structure which I think is useful to get the proper hierarchy for the records having self-relationship or child relationships. In this example, I want to display a complete view of all Accounts as well as a hierarchy for accounts within a single tree itself.

With LWC, I am able to make this tree structure minimum lines of code in my HTML component by using <lightning-tree></lightning-tree>. With the help of this component, it displays the related contacts, and cases for an Account. I have also created a self lookup relationship on Account which will have a hierarchy of Accounts, and this hierarchy will be displayed in the same tree structure along with other relationships.

Below is the code:

DynamicTreeStructureController.cls

/**
 * @description       : 
 * @author            : @tandonprateek
 * @group             : 
 * @last modified on  : 04-13-2022
 * @last modified by  : @tandonprateek
**/
public with sharing class DynamicTreeStructureController {
    public DynamicTreeStructureController() {

    }

    private static Map<Id, TreeStructure> result;
    private static Map<Id, Id> childIdMap;

    /**
    Static Method to be fed in @wire for LWC
    */
    @AuraEnabled(cacheable=true)
    public static List<TreeStructure> getAccounts(){
        result = new Map<Id, TreeStructure>();
        childIdMap = new Map<Id, Id>();
        Map<Id, Account> accMap = new Map<Id, Account>([SELECT Id, Name FROM Account WHERE ParentId = null]);
        if(!accMap.isEmpty()){
            startFetchingAccountDetails(accMap);
        }
        System.debug(JSON.serialize(result));
        return result.values();
    }

    /**
    * Recursion method to get all levels of accounts and their related records
    */
    private static List<TreeStructure> startFetchingAccountDetails(Map<Id, Account> accMap){
        Map<Id, TreeStructure> parentStructure = gatherAllAccountInformation(accMap);

        //attach the first level to actual result and rest will auotmatically link
        //due to pass by reference way
        if(result == null || result.isEmpty()){
            result.putAll(parentStructure);
        }
        Map<Id, Account> childMap = new Map<Id, Account>([SELECT Id, Name, DemoLight12__Account__c FROM Account WHERE DemoLight12__Account__c =: accMap.keySet()]);
        if(childMap != null && !childMap.isEmpty() && childMap.size() > 0){
            Map<Id, Id> accChildIdMap = new Map<Id, Id>();
            for(Id childAccountId : childMap.keySet()){
                Account child = childMap.get(childAccountId);
                childIdMap.put(child.Id, child.DemoLight12__Account__c);
            }

            //run this method recursively to get all child levels.
            List<TreeStructure> childStructure = startFetchingAccountDetails(childMap);
            for(TreeStructure child : childStructure){
                TreeStructure parent = parentStructure.get(childIdMap.get(child.name));
                parent.items.add(child);
            }
        }
        return parentStructure.values();
    }

    /**
    * Method to gather all information for all accounts recieved
    */
    private static Map<Id, TreeStructure> gatherAllAccountInformation( Map<Id, Account> accMap){
        Map<Id, TreeStructure> result = new Map<Id, TreeStructure>();

        Map<Id, List<Contact>> accConMap = new  Map<Id, List<Contact>>();
        Map<Id, List<Opportunity>> accOppCMap = new Map<Id, List<Opportunity>>();
        Map<Id, List<Case>> conCaseCMap = new Map<Id, List<Case>>();

        //gather all contacts
        for(Contact con : [SELECT Id, Name, AccountId FROM Contact WHERE AccountId =: accMap.keySet()]){
            if(!accConMap.containsKey(con.AccountId)){
                accConMap.put(con.AccountId, new List<Contact>());
            }
             accConMap.get(con.AccountId).add(con);
        }

        //gather all cases
        for(Case cas : [SELECT Id, CaseNumber, ContactId FROM Case WHERE ContactId =: accConMap.keySet()]){
            if(!conCaseCMap.containsKey(cas.ContactId)){
                conCaseCMap.put(cas.ContactId, new List<Case>());
            }
            conCaseCMap.get(cas.ContactId).add(cas);
        }

        for(Id accountId : accMap.keySet()){
            Account acc = accMap.get(accountId);
            TreeStructure accStructure = new TreeStructure(acc.name, accountId, false, null);

            //add all contacts if present
            if(accConMap.containsKey(accountId)){
                TreeStructure conStructure = new TreeStructure('Contacts', 'Contacts', false, null);
                for(Contact con :  accConMap.get(accountId)){
                    conStructure.items.add( new TreeStructure(con.Name, con.Id, false, null));
                    if(conCaseCMap.containsKey(con.Id)){
                        TreeStructure caseStructure = new TreeStructure('Cases', 'Cases', false, null);
                        for(Case cas : conCaseCMap.get(con.Id)){
                            caseStructure.items.add( new TreeStructure(cas.CaseNumber, cas.Id, false, null));
                        }
                        conStructure.items.add(caseStructure);
                    }
                }
                accStructure.items.add(conStructure);
            }

            result.put(accountId, accStructure);
        }
        return result;
    }
}
TreeStructure.cls

/**
 * @description       : 
 * @author            : @tandonprateek
 * @group             : 
 * @last modified on  : 04-13-2022
 * @last modified by  : @tandonprateek
**/
public class TreeStructure{
    @AuraEnabled public String label;
    @AuraEnabled public String name;
    @AuraEnabled public Boolean expanded;
    @AuraEnabled public List<TreeStructure> items;
    public TreeStructure(String label, String name, Boolean expanded, List<TreeStructure> items){
        this.label = label;
        this.name = name;
        this.expanded = expanded;
        if(items != null && items.size() > 0){
            this.items = items;
        }else{
            this.items = new List<TreeStructure>();
        }
    }
}

dynamicTreeStructure.html
<!--
  @description       : 
  @author            : @tandonprateek
  @group             : 
  @last modified on  : 04-13-2022
  @last modified by  : @tandonprateek
-->
<template>
    <lightning-card title="Tree Components with mutliple nested Accounts and other child records of Accounts">
      <div class="slds-m-top_medium slds-m-bottom_x-large">
        <!-- Simple -->
        <template if:true={accounts.data}>
          <div class="slds-p-around_medium lgc-bg">
            <lightning-tree items={accounts.data} header="Accounts"></lightning-tree>
          </div>
        </template>
      </div>
    </lightning-card>
  </template>
dynamicTreeStructure.js
/**
 * @description       : 
 * @author            : @tandonprateek
 * @group             : 
 * @last modified on  : 04-13-2022
 * @last modified by  : @tandonprateek
**/
import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/DynamicTreeStructureController.getAccounts';

export default class Dynamic_Tree_Structure extends LightningElement {
  @wire(getAccounts) accounts;
}
Important: Above solution is tested with 4 level hierarchy of Accounts. We need to consider the

governor limits and might need to modify the logic as per the requirements.