Forcetrails: Lazy loading in Lightning Tree Grid LWC - example with Account hierarchy

Hello Trailblazers, the Account object represents a company in Salesforce (well, most of the time). and those companies might have different hierarchies like parent-child and grandchild companies. Salesforce uses the self lookup of an Account object to store these relationships. To visualize this relationship lightning tree grid is one of the solutions if you want to visualize the data in tabular format as well as maintain the hierarchy. 

In this post, we will see how to do lazy loading of child records in the Lightning Tree Grid component from the Lightning web component framework. To demonstrate this, we will see an example of an Account hierarchy. Check out if you are not already familiar with the Lightning Tree Grid here.

Lazy loading in Lightning Tree Grid LWC


The Problem

The company structure we store in Account object can be simple or very complex, and again you might have multiple companies like that. here is one oversimplified fictitious example of that.
Different Account Hierarchies


The left side of the above image represents a company hierarchy with parent and child companies and the right side represents the internal structure of a company with different business units.


The Solution

We are going to display all the parent level Accounts at the root level of the lightning tree grid. We will query all the Accounts records with blank ParentId field value using SOQL.

Please note that you will need to implement pagination or filtering or searching features on your component in order to limit the number of records returned by SOQL queries. I am attaching reference links for that.

To query the child records down the hierarchy we will load records related to a specific parent Account account when the particular record is expanded 


Expected Output

In the below image you can see how the component will look like. Here you can see the Hyatt is a fictional parent company and it has many different children and grandchild companies. 
Please note that the children's data is not loaded until the parent is exapanded.

Account Hierarchy with Lazy Loading Tree Grid


Code

I have put all the code below in this git repo: lazy-loading-with-lightning-tree-grid-lwc

First, we will write the apex code to query the records. We will write two methods one to query the root level Accounts and another to query related child Accounts.

Apex Code

DynamicTreeGridController.cls

public with sharing class DynamicTreeGridController {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAllParentAccounts() {
        return [SELECT Name, Type FROM Account WHERE ParentId = NULL LIMIT 20];
    }

    @AuraEnabled
    public static List<Account> getChildAccounts(Id parentId) {
        return [
            SELECT Name, Type, Parent.Name
            FROM Account
            WHERE ParentId = :parentId
        ];
    }
}

Lightning web Component

dynamicTreeGrid.html

<template>
    <div>
        <lightning-tree-grid
            columns={gridColumns}
            data={gridData}
            is-loading={isLoading}
            key-field="Id"
            ontoggle={handleOnToggle}
        ></lightning-tree-grid>
    </div>
</template>

dynamicTreeGrid.js

import { LightningElement, wire } from "lwc";
import { ShowToastEvent } from "lightning/platformShowToastEvent";

// Import the schema
import ACCOUNT_NAME from "@salesforce/schema/Account.Name";
import PARENT_ACCOUNT_NAME from "@salesforce/schema/Account.Parent.Name";
import TYPE from "@salesforce/schema/Account.Type";

// Import Apex
import getAllParentAccounts from "@salesforce/apex/DynamicTreeGridController.getAllParentAccounts";
import getChildAccounts from "@salesforce/apex/DynamicTreeGridController.getChildAccounts";

// Global Constants
const COLS = [
    { fieldName: "Name", label: "Account Name" },
    { fieldName: "ParentAccountName", label: "Parent Account" },
    { fieldName: "Type", label: "Account Type" }
];

export default class DynamicTreeGrid extends LightningElement {
    gridColumns = COLS;
    isLoading = true;
    gridData = [];

    @wire(getAllParentAccounts, {})
    parentAccounts({ error, data }) {
        if (error) {
            console.error("error loading accounts", error);
        } else if (data) {
            this.gridData = data.map((account) => ({
                _children: [],
                ...account,
                ParentAccountName: account.Parent?.Name
            }));
            this.isLoading = false;
        }
    }

    handleOnToggle(event) {
        console.log(event.detail.name);
        console.log(event.detail.hasChildrenContent);
        console.log(event.detail.isExpanded);
        const rowName = event.detail.name;
        if (!event.detail.hasChildrenContent && event.detail.isExpanded) {
            this.isLoading = true;
            getChildAccounts({ parentId: rowName })
                .then((result) => {
                    console.log(result);
                    if (result && result.length > 0) {
                        const newChildren = result.map((child) => ({
                            _children: [],
                            ...child,
                            ParentAccountName: child.Parent?.Name
                        }));
                        this.gridData = this.getNewDataWithChildren(
                            rowName,
                            this.gridData,
                            newChildren
                        );
                    } else {
                        this.dispatchEvent(
                            new ShowToastEvent({
                                title: "No children",
                                message: "No children for the selected Account",
                                variant: "warning"
                            })
                        );
                    }
                })
                .catch((error) => {
                    console.log("Error loading child accounts", error);
                    this.dispatchEvent(
                        new ShowToastEvent({
                            title: "Error Loading Children Accounts",
                            message: error + " " + error?.message,
                            variant: "error"
                        })
                    );
                })
                .finally(() => {
                    this.isLoading = false;
                });
        }
    }

    getNewDataWithChildren(rowName, data, children) {
        return data.map((row) => {
            let hasChildrenContent = false;
            if (
                Object.prototype.hasOwnProperty.call(row, "_children") &&
                Array.isArray(row._children) &&
                row._children.length > 0
            ) {
                hasChildrenContent = true;
            }

            if (row.Id === rowName) {
                row._children = children;
            } else if (hasChildrenContent) {
                this.getNewDataWithChildren(rowName, row._children, children);
            }
            return row;
        });
    }
}

dynamicTreeGrid.js-meta-xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
        <!--<target>lightning__Tab</target>-->
        <!--<target>lightning__Inbox</target>-->
        <!--<target>lightning__UtilityBar</target>-->
        <!--<target>lightning__FlowScreen</target>-->
        <!--<target>lightningSnapin__ChatMessage</target>-->
        <!--<target>lightningSnapin__Minimized</target>-->
        <!--<target>lightningSnapin__PreChat</target>-->
        <!--<target>lightningSnapin__ChatHeader</target>-->
        <!--<target>lightningCommunity__Page</target>-->
        <!--<target>lightningCommunity__Default</target>-->
    </targets>
</LightningComponentBundle>


Additional Implementations

You can further customize this prototype component as per your need. Even if you can use it to show multiple related objects for example the below structure

  • Account
    • Opportunities
      • Olis
    • Contacts
    • Any other custom objects

To do this kind of implementation with multiple objects you will have to write separate apex functions to query the records. Also, you will need to call these methods dynamically based on the type the row that is being clicked. I have done a similar example where I stored all the apex methods in an array and called the methods based on the level of clicked row.

Also, I used the custom attribute on each row to identify the level, for example {..., level: "Account", "_children": [{..., level:"Contact"}]}.

You might also need to handle each apex call result differently or pass the different parameters for each level.


Important Points to Note

  • Please consider the maximum records you are going to query and limit the records based on some criteria. Remember a whole lot of data on the screen can be overwhelming for the users and also consumes more resources.
  • Consider applying filters and/or paginations to your component and load minimum required data and load more as you go.
  • A large number of rows in the grid can cause performance issues.

Let me know in the comments if any concerns or issues. Thanks :)


6 comments:
  1. Hi Rahul,
    Thank you for such a good post on Lightning Grid Tree. It has really helped me to solve my issue. Is there any way we can also change the up/down arrow icons to +/- icons for expand/collapse?

    ReplyDelete
    Replies
    1. Yes, but you need to implement the custom tree grid using slds tree grid

      Delete
    2. Here is the sample implementation for that https://github.com/salesforce/base-components-recipes/tree/master/force-app%2Fmain%2Fdefault%2Flwc%2Ftree

      Delete
  2. This is Fantastic - is there a way to modify it so that after the branch child - it no longer displays the tree grid toggle, or attempts to search for child of the branched records? If that makes sense.

    This really helped me better understand this structure.

    ReplyDelete
    Replies
    1. Yes definitely there is a way to prevent reload once the children are loaded

      Delete
    2. I'm going to play around with it a bit - that would fit my use case perfectly. Thanks much for this.

      Delete

Hi there, comments on this site are moderated, you might need to wait until your comment is published. Spam and promotions will be deleted. Sorry for the inconvenience but we have moderated the comments for the safety of this website users. If you have any concern, or if you are not able to comment for some reason, reach email us at rahul@forcetrails.com