Building a Dynamic Tree Grid with sortable functionality in LWC

May 19, 2023


 

 

In this blog post, we'll explore how to create a dynamic, tree grid component in Salesforce Lightning Web Components (LWC) that have sortable feature, row actions.

The implemention consists of two main components:

 

Component Structure

1. Main container Component(mainTreeGrid)

The component handles:

  1. Column definition
  2. Data management
  3. Sorting functionality
  4. Row actions
mainTreeGrid.html
<template>
    <lightning-card  title="Custom Tree Grid">
        <lightning-layout multiple-rows="true">
            <lightning-layout-item size="12">    
            <p class="slds-m-around_x-small">ActionItem: {actionItem}</p>
            <div class="slds-m-around_x-small slds-scrollable">
                  <c-custom-tree-grid table-columns={columns} table-data={threadList} hide-checkbox-column="true" onrowaction={handleRowAction} onsort={onSortList}></c-custom-tree-grid>
                </div>
            </lightning-layout-item>
        </lightning-layout>
    </lightning-card>
</template>
mainTreeGrid.js
import { LightningElement } from 'lwc';

const gridColumns= [
    {type: 'text', fieldName: 'col1', label: 'Name', sortable:true, link:true},
    {type: 'text', fieldName: 'col2', label: 'Email', sortable:true},
    {type: 'text', fieldName: 'col3', label: 'Phone', sortable:true},
    {type: 'text', fieldName: 'col4', label: 'Type', sortable:true},
    {type: 'text', fieldName: 'col5', label: 'Industry', sortable:true},
    {type: 'text', fieldName: 'col6', label: 'Ownership', sortable:true},
    {type: 'text', fieldName: 'col7', label: 'Rating', sortable:true},
    {type: 'text', fieldName: 'col8', label: 'CreatedDate', sortable:true},
    {type: 'action', label:'Action', typeAttributes: { rowActions: [
        { label: 'Create', name: 'create' },
        { label: 'Edit', name: 'edit' }]
    }}
];

export default class MainTreeGrid extends LightningElement {
  columns = gridColumns;
  threadList = [];
  threadSortDirection;
  threadSortedBy;
  actionItem;

  connectedCallback() {

    this.threadList = [
      {
        "Id": "1",
        "col1": "GenePoint",
        "col2": "test1@gmail.com",
        "col3": "(415) 555-1212",
        "col4": "Customer - Channel",
        "col5": "Banking",
        "col6": "Private",
        "col7": "Hot",
        "col8": "03/02/2020, 12:00 AM",
        "_children": [
          {
            "Id": "1-A",
            "col1": "Name",
            "col2": "Title",
            "col3": "Email",
            "col4": "Phone",
            "col5": "Lead Source",
            "col6": "Level",
            "col7": "Report To",
            "col8": "CreatedDate",
            "_children": [
              {
                "Id": "1-A-A",
                "col1": "Doglas",
                "col2": "SVP, Technology",
                "col3": "test0005@gmail.com",
                "col4": "(212)-555-83838",
                "col5": "Public Relations",
                "col6": "Secondary",
                "col7": "Lauren Boyle",
                "col8": "03/02/2020, 12:00 AM"
              },
              {
                "Id": "1-A-B",
                "col1": "Rogers",
                "col2": "VP, Facilities",
                "col3": "test0004@gmail.com",
                "col4": "(212)-555-83838",
                "col5": "Partner Referral",
                "col6": "Primary",
                "col7": "Scane Boyle",
                "col8": "05/03/2020, 11:00 AM"
              }
            ]
          }
        ],
      },
      {
        "Id": "2",
        "col1": "United Oil P& Gas, UAE",
        "col2": "test2@gmail.com",
        "col3": "(415) 842-5500",
        "col4": "Customer - Direct",
        "col5": "Energy",
        "col6": "Private",
        "col7": "Warm",
        "col8": "10/11/2019, 03:00 PM",
        "_children": [
          {
            "Id": "2-A",
            "col1": "Name",
            "col2": "Title",
            "col3": "Email",
            "col4": "Phone",
            "col5": "Lead Source",
            "col6": "Level",
            "col7": "Report To",
            "col8": "CreatedDate",
            "_children": [
              {
                "Id": "2-A-A",
                "col1": "Jane gray",
                "col2": "Dean of Administration",
                "col3": "test007@gmail.com",
                "col4": "(212)-125-83838",
                "col5": "Phone Inquiry",
                "col6": "Primary",
                "col7": "Jane gray",
                "col8": "05/03/2020, 11:00 AM"
              },
              {
                "Id": "2-A-B",
                "col1": "Atom smith",
                "col2": "SVP, Technology",
                "col3": "test008@gmail.com",
                "col4": "(212)-555-83838",
                "col5": "Web",
                "col6": "Tertiary",
                "col7": "Atom smith",
                "col8": "05/05/2021, 03:00 AM"
              }
            ]
          }
        ],
      }
    ]
  }

  handleRowAction(event) {
    this.actionItem = JSON.stringify(event.detail.row);
  }

  handleRowSelect(event) {
    this.actionItem = JSON.stringify(event.detail.row);
  }

  onSortList(event) {
    let fieldName = event.detail.sortedBy;
    let sortDirection = event.detail.sortDirection;
    this.sortData(fieldName, sortDirection);
    this.sortBy = fieldName;
    this.sortDirection = sortDirection;
  }

  sortData(fieldName, sortDirection) {
    let sortResult = this.threadList;
    let sorted = sortResult.sort(function (a, b) {
    if (a[fieldName] < b[fieldName])
        return sortDirection === 'desc' ? 1 : -1;
    else if (a[fieldName] > b[fieldName])
        return sortDirection === 'desc' ? -1 : 1;
    else
        return 0;
    })
    this.threadList = JSON.parse(JSON.stringify(sorted));
  }
}
mainTreeGrid.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>63.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__HomePage</target>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>

 

2. Custom Tree Grid Component (customTreeGrid)

The custom tree grid component implements the actual grid functionality:

  1. Resizable columns
  2. Sortable headers
  3. Tree structure support
  4. Dynamic width adjustment
  5. Row actions menu

 

customTreeGrid.html
<template>

    <div class="slds-table_header-fixed_container slds-scrollable_x" onmousemove={handlemousemove} onmouseup={handlemouseup} ondblclick={handledblclickresizable}>
        <table aria-multiselectable="true" class="slds-table slds-table_header-fixed slds-table_bordered slds-table_edit slds-table_resizable-cols slds-tree slds-table_tree" role="treegrid" aria-label="Example default tree grid" style="table-layout: fixed;">
            <thead>
                <tr class="slds-line-height_reset">
                    <template lwc:if={hideColumn}>
                        <th class="slds-text-align_right" scope="col" style="width:2rem"></th>
                    </template>
                    <template for:each={labelList} for:item="col">
                        <template lwc:if={col.sortable}>
                            <th key={col.fieldName} aria-sort="none" style={fixedWidth} class="slds-is-resizable dv-dynamic-width slds-has-button-menu slds-is-resizable slds-is-sortable" scope="col">
                                <div class="slds-cell-fixed" style={fixedWidth}>
                                    <a class="slds-th__action slds-text-link_reset" href="#" role="button" tabindex="-1" onclick={sortByColumn}>
                                        <span class="slds-assistive-text">Sort by: </span>
                                        <div class="slds-grid slds-grid_vertical-align-center slds-has-flexi-truncate" data-icon-name={col.sortIconTitle} onmouseenter={mouseEnter} onmouseout={mouseOut} onmouseleave={mouseLeave} data-label={col.fieldName}>
                                            <span class="slds-truncate" data-label={col.fieldName} data-icon-name={col.sortIconTitle} title={col.label}>{col.label}</span>
                                            <span class="slds-m-left_xx-small slds-icon_container slds-icon-utility-arrowdown" >
                                                <lightning-icon class={col.hoverClass} icon-name={col.sortIconName}  alternative-text={col.sortIconTitle} size='xx-small' data-label={col.fieldName} data-icon-name={col.sortIconTitle} title={col.sortIconTitle}></lightning-icon>
                                            </span>
                                        </div>
                                    </a>
                                    <button class="slds-button slds-button_icon slds-th__action-button slds-button_icon-x-small" aria-haspopup="true" tabindex="-1">
                                        <lightning-icon icon-name='utility:chevrondown' alternative-text='chevrondown' size='xx-small' title='chevrondown'></lightning-icon>
                                    </button>
                                    <div class="slds-resizable">
                                        <span class="slds-resizable__handle" onmousedown={handlemousedown}>
                                            <span class="slds-resizable__divider"></span>
                                        </span>
                                    </div>
                                </div>
                            </th>
                        </template>
                        <template lwc:else>
                            <th key={col.fieldName} aria-sort="none" style={fixedWidth} class="slds-is-resizable dv-dynamic-width slds-has-button-menu slds-is-resizable slds-is-sortable" scope="col">
                                <div class="slds-cell-fixed" style={fixedWidth}>
                                    <a class="slds-th__action slds-text-link_reset" href="#" role="button" tabindex="-1" >
                                        <span class="slds-assistive-text">Sort by: </span>
                                        <div class="slds-grid slds-grid_vertical-align-center slds-has-flexi-truncate" >
                                            <span class="slds-truncate" data-label={col.fieldName} title={col.label}>{col.label}</span>
                                        </div>
                                    </a>
                                    <button class="slds-button slds-button_icon slds-th__action-button slds-button_icon-x-small" aria-haspopup="true" tabindex="-1" title="Show column actions">
                                        <lightning-icon icon-name='utility:chevrondown' alternative-text='chevrondown' size='xx-small' title='chevrondown'></lightning-icon>
                                    </button>
                                    <div class="slds-resizable">
                                        <span class="slds-resizable__handle" onmousedown={handlemousedown}>
                                            <span class="slds-resizable__divider"></span>
                                        </span>
                                    </div>
                                </div>
                            </th>
                        </template>
                    </template>
                </tr>
            </thead>
            <tbody>
                <template for:each={dataList} for:item="data" for:index="index">
                    <tr class="slds-hint-parent" key={data.Id} id={data.Id} aria-expanded={data.isExpand} aria-level="1" aria-posinset="2" aria-selected="false" aria-setsize="4" data-row-key-value={data.Id} data-row-number={data.rowNumber}>
                        <template lwc:if={hideColumn}>
                            <td class="slds-text-align_right" role="gridcell" style="width:3.25rem">
                                <div class="slds-checkbox">
                                    <lightning-input type="checkbox" checked={data.isSelected} onchange={singleSelection}  data-id={data.Id} name={data.Id}></lightning-input>
                                </div>
                            </td>
                        </template>
                        <td class="slds-tree__item" data-label={data.col1} scope="row">
                            <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={data.col1}>
                                <button class="slds-button slds-button_icon slds-button_icon-xx-small slds-m-right_x-small" aria-hidden="false" tabindex="-1" title={data.col1} onclick={toggleExpand}>
                                    <lightning-icon icon-name={data.expandIconName} alternative-text={data.expandIcontitle} size='xx-small' title={data.expandIcontitle} data-row-title={data.col1} data-row-key-value={data.Id} data-row-number={data.rowNumber} data-aria-expanded={data.isExpand}></lightning-icon>
                                    <span class="slds-assistive-text">{data.col1}</span>
                                </button>
                                <a href={data.smturl} tabindex="-1">{data.col1}</a>
                            </div>
                        </td>
                        <td data-label={data.col2} role="gridcell">
                            <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={data.col2}>{data.col2}</div>
                        </td>
                        <td data-label={data.col3} role="gridcell">
                            <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={data.col3}>{data.col3}</div>
                        </td>
                        <td data-label={data.col4} role="gridcell">
                            <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={data.col4}>{data.col4}</div>
                        </td>
                        <td data-label={data.col5} role="gridcell">
                            <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={data.col5}>{data.col5}</div>
                        </td>
                        <td data-label={data.col6} role="gridcell">
                            <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={data.col6}>{data.col6}</div>
                        </td>
                        <td data-label={data.col7} role="gridcell">
                            <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={data.col7}>{data.col7}</div>
                        </td>
                        <td data-label={data.col8} role="gridcell">
                            <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={data.col8}>{data.col8}</div>
                        </td>
                        <td role="gridcell" class="slds-cell_action-mode">
                            <lightning-button-menu menu-alignment="auto" alternative-text="Show menu" variant="border-filled" icon-size="x-small" >
                                <template for:each={data.rowAction} for:item="action" for:index="index">
                                    <lightning-menu-item key={data.Id} value={data} label={action.label} data-menu-label={action.label} data-menu-name={action.name} data-menu-id={data.Id} onclick={clickmenuitem}></lightning-menu-item>
                                </template>
                            </lightning-button-menu>
                        </td>
                    </tr>

                    <template lwc:if={data._children}>
                        <template lwc:if={data.isExpand}>
                            <template for:each={data._children} for:item="child1">
                                <tr key={data.Id} id={child1.Id} aria-expanded={child1.isExpand} aria-level="2" aria-posinset="1" aria-selected="false" aria-setsize="1" class="slds-hint-parent" data-row-key-value={child1.Id} data-row-number={child1.rowNumber}>
                                    <template lwc:if={hideColumn}>
                                        <th class="slds-text-align_right" role="gridcell" style="width:3.25rem">
                                            <!-- <template lwc:if={showChildHeader}>
                                                <div class="slds-checkbox">
                                                    <lightning-input type="checkbox" checked={child1.isSelected} id={child1.Id} name={child1.Id}></lightning-input>
                                                </div>
                                            </template> -->
                                        </th>
                                    </template>
                                    <td class="slds-tree__item" data-label={child1.col1} scope="row">
                                        <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={child1.col1}>
                                            <button class="slds-button slds-button_icon slds-button_icon-xx-small slds-m-right_x-small" aria-hidden="false" tabindex="-1" title={child1.col1} onclick={toggleExpand}>
                                                <lightning-icon icon-name={child1.expandIconName} alternative-text={child1.expandIcontitle} size='xx-small' title={child1.expandIcontitle} data-row-title={child1.col1} data-row-key-value={child1.Id} data-row-number={child1.rowNumber} data-aria-expanded={data.isExpand}></lightning-icon>
                                                <span class="slds-assistive-text">{child1.col1}</span>
                                            </button>
                                            <span tabindex="-1" class="slds-text-title_bold">{child1.col1}</span>
                                        </div>
                                    </td>
                                    <td data-label={child1.col2} role="gridcell">
                                        <div class="slds-truncate dv-dynamic-width slds-text-title_bold" style={fixedWidth} title={child1.col2}>{child1.col2}</div>
                                    </td>
                                    <td data-label={child1.col3} role="gridcell">
                                        <div class="slds-truncate dv-dynamic-width slds-text-title_bold" style={fixedWidth} title={child1.col3}>{child1.col3}</div>
                                    </td>
                                    <td data-label={child1.col4} role="gridcell">
                                        <div class="slds-truncate dv-dynamic-width slds-text-title_bold" style={fixedWidth} title={child1.col4}>{child1.col4}</div>
                                    </td>
                                    <td data-label={child1.col5} role="gridcell">
                                        <div class="slds-truncate dv-dynamic-width slds-text-title_bold" style={fixedWidth} title={child1.col5}>{child1.col5}</div>
                                    </td>
                                    <td data-label={child1.col6} role="gridcell">
                                        <div class="slds-truncate dv-dynamic-width slds-text-title_bold" style={fixedWidth} title={child1.col6}>{child1.col6}</div>
                                    </td>
                                    <td data-label={child1.col7} role="gridcell">
                                        <div class="slds-truncate dv-dynamic-width slds-text-title_bold" style={fixedWidth} title={child1.col7}>{child1.col7}</div>
                                    </td>
                                    <td data-label={child1.col8} role="gridcell">
                                        <div class="slds-truncate dv-dynamic-width slds-text-title_bold" style={fixedWidth} title={child1.col8}>{child1.col8}</div>
                                    </td>
                                    <td role="gridcell" style="width:3.25rem"></td>
                                </tr>
                               
                                <template lwc:if={child1._children}>
                                    <template lwc:if={child1.isExpand}>
                                        <template for:each={child1._children} for:item="child2" for:index="index">
                                            <tr key={child2.Id} id={child2.Id} aria-expanded={child2.isExpand} aria-level="3" aria-posinset="1" aria-selected="false" aria-setsize="1" class="slds-hint-parent" data-row-key-value={child2.Id} data-row-number={child2.rowNumber}>
                                                <template lwc:if={hideColumn}>
                                                    <td class="slds-text-align_right" role="gridcell" style="width:3.25rem">
                                                        <div class="slds-checkbox">
                                                            <lightning-input type="checkbox" checked={child2.isSelected} onchange={singleSelection} data-id={child2.Id} name={child2.Id}></lightning-input>
                                                        </div>
                                                    </td>
                                                </template>
                                                <td class="slds-tree__item" data-label={child2.col1} scope="row">
                                                    <button class="slds-button slds-button_icon slds-button_icon-xx-small slds-m-right_x-small" aria-hidden="false" tabindex="-1" title={child2.col1} onclick={toggleExpand}>
                                                        <lightning-icon icon-name={child2.expandIconName} alternative-text={child2.expandIcontitle} size='xx-small' title={child2.expandIcontitle} data-row-title={child2.col1} data-row-key-value={child2.Id} data-row-number={child2.rowNumber}></lightning-icon>
                                                        <span class="slds-assistive-text">{child2.col1}</span>
                                                    </button>
                                                    <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={child2.col1}>
                                                        <a href={child2.smturl} tabindex="-1">{child2.col1}</a>
                                                    </div>
                                                </td>
                                                <td data-label={child2.col2} role="gridcell">
                                                    <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={child2.col2}>{child2.col2}</div>
                                                </td>
                                                <td data-label={child2.col3} role="gridcell">
                                                    <div class="slds-truncate" title={child2.col3}>{child2.col3}</div>
                                                </td>
                                                <td data-label={child2.col4} role="gridcell">
                                                    <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={child2.col4}>{child2.col4}</div>
                                                </td>
                                                <td data-label={child2.col5} role="gridcell">
                                                    <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={child2.col5}>{child2.col5}</div>
                                                </td>
                                                <td data-label={child2.col6} role="gridcell">
                                                    <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={child2.col6}>{child2.col6}</div>
                                                </td>
                                                <td data-label={child2.col7} role="gridcell">
                                                    <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={child2.col7}>{child2.col7}</div>
                                                </td>
                                                <td data-label={child2.col8} role="gridcell">
                                                    <div class="slds-truncate dv-dynamic-width" style={fixedWidth} title={child2.col8}>{child2.col8}</div>
                                                </td>
                                                <td role="gridcell" class="slds-cell_action-mode">
                                                    <lightning-button-menu menu-alignment="auto" alternative-text="Show menu" variant="border-filled" icon-size="x-small">
                                                        <template for:each={child2.rowAction} for:item="action" for:index="index">
                                                            <lightning-menu-item key={child2.Id} value={child2} label={action.label} data-menu-label={action.label} data-menu-name={action.name} data-menu-id={child2.Id} onclick={clickmenuitem}></lightning-menu-item>
                                                        </template>
                                                    </lightning-button-menu>

                                                </td>
                                            </tr>
                                        </template>
                                    </template>
                                </template>

                            </template>
                        </template>
                    </template>

                </template>

            </tbody>
        </table>
    </div>
</template>
customTreeGrid.js
import { LightningElement, api } from 'lwc';

export default class CustomTreeGrid extends LightningElement {
   
    @api hideCheckboxColumn;
    labelList=[];
    dataList=[];
    sortIconName = 'utility:arrowdown';
    iconTitle = 'arrowdown';
    expandIconName = 'utility:chevronright';
    expandIcontitle = 'chevronright';
    label=[];
    rowAction;
    rowActionItem={};
    fixedWidth = "width:200px";

    @api
    set tableColumns(value) {
        this.labelList = value.map(column=>{
            let labelObj = {...column}
            labelObj.hoverClass = 'slds-hide';
            labelObj.sortIconName = 'utility:arrowdown';
            labelObj.sortIconTitle = 'arrowdown';
            if(labelObj.hasOwnProperty('typeAttributes')) {
                this.rowAction = labelObj.typeAttributes.rowActions;
            }
            return labelObj;
        });
    }

    get tableColumns() {
        return this.labelList;
    }

    @api
    set tableData(value) {
        if(value != undefined) {
            this.dataList = this.iterateNestedData(value);
        }
    }

    get tableData() {
        return this.dataList;
    }

    get hideColumn() {
        if(this.hideCheckboxColumn=="true") {
            return false;
        } else {
            return true;
        }
    }

    iterateNestedData(data) {
        const dataList = data.map((item, index) => {
            let obj = {...item}
            if(obj.hasOwnProperty('_children')) {
                obj.isExpand = false;
                obj.rowNumber = index+1;
                obj.expandIconName = 'utility:chevronright';
                obj.expandIcontitle = 'chevronright';
            }
            obj.isSelected = false;
            obj.menuClass = 'slds-dropdown-trigger slds-dropdown-trigger_click';
            obj.rowAction = this.rowAction;
            if(item.hasOwnProperty('_children')) {
                obj._children = this.iterateNestedData(obj._children); // Recursive call for children
            }
            return obj;
        });
        return dataList;
    }    

    mouseEnter(event) {
        this.labelList.map(column=>{
            if(event.target.dataset.label == column.fieldName){
                column.hoverClass = 'slds-show';
            }
        });
        this.labelList = [...this.labelList];
    }

    mouseOut(event) {
        this.labelList.map(column=>{
            if(event.target.dataset.label != column.fieldName){
                column.hoverClass = 'slds-hide';
            }
        });
        this.labelList = [...this.labelList];
    }

    mouseLeave(event) {
        this.labelList.map(column=>{
            if(event.target.dataset.label == column.fieldName){
                column.hoverClass = 'slds-hide';
            }
        });
        this.labelList = [...this.labelList];
    }

    sortByColumn(event) {
        const columnname = event.target.dataset.label
        const iconName = event.target.dataset.iconName;
        let sortDirection = 'asc';
        this.labelList.map(column=>{
            if(event.target.dataset.label == column.fieldName && column.sortIconName == 'utility:arrowdown'){
                column.sortIconName = 'utility:arrowup';
                column.sortIconTitle = 'arrowup';
            } else {
                column.sortIconName = 'utility:arrowdown';
                column.sortIconTitle = 'arrowdown';
            }
        });
        this.labelList = [...this.labelList];
        if(iconName === 'arrowdown') {
            sortDirection = 'asc';
        } else {
            sortDirection =  'desc';
        }
        const columnEvent = new CustomEvent("sort", { detail:{sortedBy:columnname, sortDirection:sortDirection }});
        this.dispatchEvent(columnEvent);
    }

    selectedrow=null;
    singleSelection(event) {
        let singleselection = event.target.checked;
        let rowid = event.target.dataset.Id;
        this.dataList = this.selecteSingleRow(this.dataList, rowid);
        if(this.selectedrow != null && singleselection) {
            const rowselect = new CustomEvent("rowselect", { detail:this.selectedrow});
            this.dispatchEvent(rowselect);
        }
    }

    selecteSingleRow(data, rowid) {
        const dataList = data.map((item, index) => {
            if(rowid==item.Id)
                item.isSelected = true
            else {
                item.isSelected = false;
            }
            if(item.hasOwnProperty('_children')) {
                item._children = this.selecteSingleRow(item._children, rowid); // Recursive call for children
            }
            if(item.Id==rowid) {
                this.selectedrow = {...this.selectedrow, Id:item.Id, col1:item.col1, col2:item.col2, col3:item.col3, col4:item.col4, col5:item.col5, col6:item.col6, col7:item.col7, col8:item.col8, col9:item.col9, col10:item.col10, smtId:item.smtId};
            }
            return item;
           
        });
        return dataList;
    }

   
    toggleExpand(event) {
        const rowKeyValue = event.target.dataset.rowKeyValue;
        this.dataList = this.nestedData(this.dataList, rowKeyValue);
    }
   
    nestedData(data, rowKeyValue) {
        const dataList = data.map((item, index) => {
            if(item.hasOwnProperty('_children')) {
                if((item.Id === rowKeyValue) && (item.isExpand === false)) {
                    item.isExpand = true;
                    item.expandIconName = 'utility:chevrondown';
                    item.expandIconTitle = 'chevrondown';
                }
                else if((item.Id === rowKeyValue) && (item.isExpand === true)) {
                    item.isExpand = false;
                    item.expandIconName = 'utility:chevronright';
                    item.expandIconTitle = 'chevronright';
                }
            }
            if(item.hasOwnProperty('_children')) {
                item._children = this.nestedData(item._children, rowKeyValue); // Recursive call for children
            }
            return item;
        });
        return dataList;
    }

    clickmenuitem(event) {
        const rowId = event.target.dataset.menuId;
        this.menuRowAction(this.dataList, rowId);
        const selectedItem = {action:{label:event.target.dataset.menuLabel, name:event.target.dataset.menuName}, row:this.rowActionItem}
        if(this.rowActionItem != null) {
            const actionEvent = new CustomEvent("rowaction", { detail:selectedItem});
            this.dispatchEvent(actionEvent);
        }
    }

    menuRowAction(data, rowid) {
        const dataList = data.map((item, index) => {
            if(item.hasOwnProperty('_children')) {
                item._children = this.menuRowAction(item._children, rowid); // Recursive call for children
            }
            if(item.Id==rowid) {
                this.rowActionItem = {...this.rowActionItem, Id:item.Id, col1:item.col1, col2:item.col2, col3:item.col3, col4:item.col4, col5:item.col5, col6:item.col6, col7:item.col7, col8:item.col8, col9:item.col9, col10:item.col10, smtId:item.smtId};
            }
            return item;
        });
        return dataList;
    }

    handlemouseup(e) {
        this._tableThColumn = undefined;
        this._tableThInnerDiv = undefined;
        this._pageX = undefined;
        this._tableThWidth = undefined;
    }
   
    handlemousedown(e) {
        if (!this._initWidths) {
            this._initWidths = [];
            let tableThs = this.template.querySelectorAll("table thead .dv-dynamic-width");
        }
   
        this._tableThColumn = e.target.parentElement;
        this._tableThInnerDiv = e.target.parentElement;
        while (this._tableThColumn.tagName !== "TH") {
            this._tableThColumn = this._tableThColumn.parentNode;
        }
        while (!this._tableThInnerDiv.className.includes("slds-cell-fixed")) {
            this._tableThInnerDiv = this._tableThInnerDiv.parentNode;
        }
        this._pageX = e.pageX;
        this._padding = this.paddingDiff(this._tableThColumn);
   
        this._tableThWidth = this._tableThColumn.offsetWidth - this._padding;
    }
   
    handlemousemove(e) {
        if (this._tableThColumn && this._tableThColumn.tagName === "TH") {
            this._diffX = e.pageX - this._pageX;
   
            this.template.querySelector("table").style.width = (this.template.querySelector("table") - (this._diffX)) + 'px';
   
            this._tableThColumn.style.width = (this._tableThWidth + this._diffX) + 'px';
            this._tableThInnerDiv.style.width = this._tableThColumn.style.width;
   
            let tableThs = this.template.querySelectorAll("table thead .dv-dynamic-width");
            let tableBodyRows = this.template.querySelectorAll("table tbody tr");
            let tableBodyTds = this.template.querySelectorAll("table tbody .dv-dynamic-width");
            tableBodyRows.forEach(row => {
                let rowTds = row.querySelectorAll(".dv-dynamic-width");
                rowTds.forEach((td, ind) => {
                    rowTds[ind].style.width = tableThs[ind].style.width;
                });
            });
        }
    }
   
    handledblclickresizable() {
        let tableThs = this.template.querySelectorAll("table thead .dv-dynamic-width");
        let tableBodyRows = this.template.querySelectorAll("table tbody tr");
        tableThs.forEach((th, ind) => {
            th.style.width = this._initWidths[ind];
            th.querySelector(".slds-cell-fixed").style.width = this._initWidths[ind];
        });
        tableBodyRows.forEach(row => {
            let rowTds = row.querySelectorAll(".dv-dynamic-width");
            rowTds.forEach((td, ind) => {
                rowTds[ind].style.width = this._initWidths[ind];
            });
        });
    }
   
    paddingDiff(col) {
   
        if (this.getStyleVal(col, 'box-sizing') === 'border-box') {
            return 0;
        }
   
        this._padLeft = this.getStyleVal(col, 'padding-left');
        this._padRight = this.getStyleVal(col, 'padding-right');
        return (parseInt(this._padLeft, 10) + parseInt(this._padRight, 10));
   
    }
   
    getStyleVal(elm, css) {
        return (window.getComputedStyle(elm, null).getPropertyValue(css))
    }
}
customTreeGrid.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>63.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>

 

Output:

 

Conclusion

This implementation provides a robust, flexible tree grid component that can be easily integrated into any Salesforce LWC application. The component offers a rich set of features while maintaining good performance and user experience.