Pages

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.