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.
The Problem
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.
- Pagination using Salesforce Lightning Web Components with array slicing
- Filtered & Searchable datatable in LWC with the wired method
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.
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 :)
Hi Rahul,
ReplyDeleteThank 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?
Yes, but you need to implement the custom tree grid using slds tree grid
DeleteHere is the sample implementation for that https://github.com/salesforce/base-components-recipes/tree/master/force-app%2Fmain%2Fdefault%2Flwc%2Ftree
DeleteThis 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.
ReplyDeleteThis really helped me better understand this structure.
Yes definitely there is a way to prevent reload once the children are loaded
DeleteI'm going to play around with it a bit - that would fit my use case perfectly. Thanks much for this.
DeleteGood work Rahul, useful for similar kinda requirements too
ReplyDeleteThank you very much Vikas!
Delete