6

I have searched previous questions and believe my issue is unique. There are questions that are similar, but not exactly what I am trying to solve.

I am creating a custom LWC component to display a table. The data for this table is not tabular. It is a hierarchy/tree structure. The tree data can have many levels of nesting.

In a perfect world, all I would need to do is create a recursive LWC component and then iterate through the top level nodes as shown below:

<tbody>
    <template for:each={treeData} for:item="rootNode">
        <c-tree-node key={rootNode.id} node={rootNode}></c-tree-node>
    </template>
</tbody>

with <c-tree-node> being implemented as below:

<tr>
    <td>{node.field_1__c}</td>
    <td>{node.field_2__c}</td>
</tr>

<template lwc:if={node.hasChildren}>
    <template for:each={node.children} for:item="childNode">
        <c-tree-node key={childNode.id} node={childNode}></c-tree-node>
    </template>
</template>

Unfortunately, the limitations of LWC with dynamic table rows force me to take one of three approaches.

The first approach is to remove the <tr> tag from the <c-tree-node> component and then modify the display property of the host element as follows:

:host {
    display: table-row;
}

This approach is unacceptable because that confines all markup for this component to be a single table row. So it does not support creating <tr>s for the child nodes.

The second approach is to not make a recursive component and instead hard code each level of possible nesting in the main iteration. The code below highlights that:

<tbody>
    <template for:each={treeData} for:item="rootNode">

        <tr key={rootNode.id}>
            <td>{rootNode.field_1__c}</td>
            <td>{rootNode.field_2__c}</td>
        </tr>

        <template lwc:if={rootNode.hasChildren}>

            <template for:each={rootNode.children} for:item="level1Node">

                <tr key={level1Node.id}>
                    <td>{level1Node.field_1__c}</td>
                    <td>{level1Node.field_2__c}</td>
                </tr>

                <template lwc:if={level1Node.hasChildren}>

                    <template for:each={level1Node.children} for:item="level2Node">
                        <tr key={level2Node.id}>
                            <td>{level2Node.field_1__c}</td>
                            <td>{level2Node.field_2__c}</td>
                        </tr>
                    </template>
                </template>
            </template>
        </template>
    </template>
</tbody>

The second approach DOES WORK. I just do not like it one bit because the code gets very verbose and repetitive when you try to support multiple levels of nesting. If code is not exactly the same for each level of nesting it can lead to some nasty head scratchers. My actual implementation details are way more complex than two basic <td> tags for each row. I have dynamic columns and complex parent/child calculations to think about. A recursive component would dramatically reduce complexity for the calculations. When I write code I try to think about making it easy to understand for anyone who has to support it later.

The third approach is to completely flatten the tree data into a tabular structure and just iterate over that. That would look as follows:

<tbody>
    <template for:each={flattenedTreeData} for:item="row">
        <c-flat-row key={row.id} node={row}></c-flat-row>
    </template>
</tbody>

with c-flat-row implemented as follows:

<td>{node.field_1__c}</td>
<td>{node.field_2__c}</td>
:host {
    display: table-row;
}

To be completely upfront, I am not even considering this third approach because it adds too much complexity to the requirements that I am building up to. Those requirements are outside the scope of this post but they can be summed up by saying that I need to perform calculations on a <tr> that require input from the parent and child <tr>s. The complexity I just mentioned is also a trade-off for the second approach but this also has the added complexity of ordering when the user needs to sort the data. I know that I could get it to work but I am concerned that the end result would be a maintenance nightmare for the devs who come after me. A recursive component would be ideal because of how event communication works.

So my question to the community is, is there a better approach than what I have listed or is there some improvement I can make to the approaches above to achieve a truly dynamic data table with tree data?

UPDATE Adding some example data per request in comments. It is an array of objects. Notice that some object have child objects. A basic tree structure.

[
    {
        id: 'aaa',
        Field_1__c: 'text here',
        Field_2__c: 'text here',
    },
    {
        id: 'bbb',
        Field_1__c: 'text here',
        Field_2__c: 'text here',
        hasChildren: true,
        children:[
            {
                id: 'ccc',
                Field_1__c: 'text here',
                Field_2__c: 'text here',
            },
            {
                id: 'ddd',
                Field_1__c: 'text here',
                Field_2__c: 'text here',
                hasChildren: true,
                children:[{
                    id: 'eee',
                    Field_1__c: 'text here',
                    Field_2__c: 'text here',
                }]
            },
        ]
    }
]
7
  • "Unfortunately, the limitations of LWC with dynamic table rows force me to take one of three approaches." Can you expand on this part more, I dont really understand what you mean by this. Also can you share a sample of the data you want to iterate / nest over Apr 6, 2023 at 20:27
  • Thank you for challenging me on that @BryanAnderson. Please refer to this readme file where I outline that issue in detail. It is definitely a pre-requisite concept for this issue. github.com/robertStrunk/Dynamic-Table-Row-Limitation. I'll modify the question above to provide some example data.
    – rStrunk
    Apr 9, 2023 at 16:39
  • I think tables are considered bad these days. I recently had to implement a tree grid on another popular front-end framework and ended up using div's instead. You may also need to consider "windowing" for performance if you have a large data set. Apr 10, 2023 at 9:37
  • Why are you not using the treegrid? it has great functionality to fulfil your requirements.
    – tejas
    Apr 11, 2023 at 18:36
  • That's 8 years old. People are realising that if you have tabular data you should use a table. Apr 11, 2023 at 18:43

2 Answers 2

2
+400

That was a though one, but I found a solution which actually works and is actually the perfect world scenario you've described.

In your attached Github document you are describing limitations of LWC but it's actually a limitation of tables and unknown elements inside of tbody, tr or wherever (todo: find a valid source for this claim 😉). but with just a little CSS you could have fixed that.

Table approach with child LWC

I've created two LWC components, one is the parent, holding the table and the data, the other one represents a row (and its children)

HTML for the parent component

<!-- Table Lwc -->
<template>
    <table>
        <thead>
        <tr>
            <th>Column 1</th>
            <th>Column 2</th>
        </tr>
        </thead>
        <tbody>
        <template for:each={treeData} for:item="row">
            <c-table-row key={row.id} row-data={row}></c-table-row>
        </template>
        </tbody>
    </table>
</template>

JS for the parent component

import { LightningElement, track } from 'lwc';

export default class TableLwc extends LightningElement {
    @track treeData = [
        {
            id: 'aaa',
            Field_1__c: 'aaa text here 1',
            Field_2__c: 'aaa text here 2',
        },
        {
            id: 'bbb',
            Field_1__c: 'bbb text here 1',
            Field_2__c: 'bbb text here 2',
            hasChildren: true,
            children:[
                {
                    id: 'ccc',
                    Field_1__c: 'ccc text here 1',
                    Field_2__c: 'ccc text here 2',
                },
                {
                    id: 'ddd',
                    Field_1__c: 'ddd text here 1',
                    Field_2__c: 'ddd text here 2',
                    hasChildren: true,
                    children:[{
                        id: 'eee',
                        Field_1__c: 'eee text here 1',
                        Field_2__c: 'eee text here 2',
                    }]
                },
            ]
        }
    ];
}

(I've extended your example data slightly to better understand the output in the browser)

HTML for the child component

<!-- Table Row -->
<template>
        <tr>
            <td>{rowData.Field_1__c}</td>
            <td>{rowData.Field_2__c}</td>
        </tr>
    <template if:true={rowData.hasChildren}>
        <template for:each={rowData.children} for:item="child">
            <c-table-row key={child.id} row-data={child}></c-table-row>
        </template>
    </template>
</template>

JS for the child component

import { LightningElement, api } from 'lwc';

export default class TableRow extends LightningElement {
    @api rowData;
}

(you see, pretty close to your approach so far)

CSS for the child component

:host {
    display: contents;
}

This little css does the trick. So the resulting HTML will look like this

<table>
    <thead>
        <tr>
            <th>Column 1</th>
            <th>Column 2</th>
        </tr>
    </thead>
    <tbody>
        <c-table-row c-tablerow_tablerow-host="">
            <tr c-tablerow_tablerow="">
                <td c-tablerow_tablerow="">aaa text here 1</td>
                <td c-tablerow_tablerow="">aaa text here 2</td>
            </tr>
        </c-table-row>
        <c-table-row c-tablerow_tablerow-host="">
            <tr c-tablerow_tablerow="">
                <td c-tablerow_tablerow="">bbb text here 1</td>
                <td c-tablerow_tablerow="">bbb text here 2</td>
            </tr>
            <c-table-row c-tablerow_tablerow="" c-tablerow_tablerow-host="">
                <tr c-tablerow_tablerow="">
                    <td c-tablerow_tablerow="">ccc text here 1</td>
                    <td c-tablerow_tablerow="">ccc text here 2</td>
                </tr>
            </c-table-row>
            <c-table-row c-tablerow_tablerow="" c-tablerow_tablerow-host="">
                <tr c-tablerow_tablerow="">
                    <td c-tablerow_tablerow="">ddd text here 1</td>
                    <td c-tablerow_tablerow="">ddd text here 2</td>
                </tr>
                <c-table-row c-tablerow_tablerow="" c-tablerow_tablerow-host="">
                    <tr c-tablerow_tablerow="">
                        <td c-tablerow_tablerow="">eee text here 1</td>
                        <td c-tablerow_tablerow="">eee text here 2</td>
                    </tr>
                </c-table-row>
            </c-table-row>
        </c-table-row>
    </tbody>
</table>

As you recognize: we have nested c-table-row elements which is because of the nested children, but it works like a charm, the html is correctly rendered and you have a child component which you can use for the other requirements you've mentioned.

Result: Result in a browser

Potential better approach

However I still want to answer your original question regarding "is there a better approach than what I have listed or is there some improvement I can make to the approaches above to achieve a truly dynamic data table with tree data?"

In my personal opinion there are lots of other approaches. Based on your need to show a tree structure as a flat table I personally would go with a lightning-datatable and custom data types if needed.

Lightning Web Components Dev Guide - Create a Custom Data Type

You can have all the freedom of child / parent communication but you still have a UI which looks like Salesforce, smells like Salesforce and works like it.

But this is just my personal opinion. Based on your need the above solution should work.

1
  • 1
    Thank you very much for your response. You took the time to truly understand the issue and absolutely crushed it. I have created custom data types for the LWC datatable in the past. I briefly considered them for this issue, but ruled it out for two reasons. The first was that, and this may no longer be the case, the custom data type did not have the ability to dispatch custom events. The second was that I felt I needed more control over the functionality of the table in order to accommodate future requirements. You clearly know your stuff so I will reevaluate custom datatypes. Thanks again.
    – rStrunk
    Apr 13, 2023 at 21:43
0

I don't know if this is efficient for you. But, I have managed to code in a single LWC and using the table tag. Variable Names are changed, but you can get the approach.

LWC-

import { LightningElement } from 'lwc';

export default class CustomTreeGrid extends LightningElement {
    data = [
        {
            id: 1, name: 'A', parentId: null, children: [
                {
                    id: 2, name: 'B', parentId: 1, children: [
                        {
                            id: 3, name: 'C', parentId: 2, children: [
                                { id: 4, name: 'D', parentId: 3, children: null }
                            ]
                        }
                    ]
                },
                {
                    id: 9, name: 'I', parentId: 1, children: null
                }
            ]
        },
        {
            id: 5, name: 'E', parentId: null, children: null
        },
        {
            id: 6, name: 'F', parentId: null, children: [
                {
                    id: 7, name: 'G', parentId: 6, children: [
                        { id: 8, name: 'H', parentId: 7, children: null }
                    ]
                }
            ]
        }
    ];

    trString = '<thead><tr><th>Id</th><th>Name</th><th>ParentId</th></tr></thead><tbody>';
    renderedCallback() {
        this.getRecords(this.data);
        this.trString += '</tbody>';
        console.log('trString', this.trString);
        this.template.querySelector('.table').innerHTML = this.trString;
    }


    getRecords(data, indent="&nbsp;") {
        data.forEach(record => {
            if (record.children != null) {
                console.log(indent + record.name);
                this.trString += '<tr>' +
                    '<td>' + record.id + '</td>' +
                    '<td>' + indent + record.name + '</td>' +
                    '<td>' + record.parentId + '</td>' +
                    '</tr>';
                this.getRecords(record.children, indent + "&nbsp;");
            }
            else {
                this.trString += '<tr>' +
                    '<td>' + record.id + '</td>' +
                    '<td>' + indent + record.name + '</td>' +
                    '<td>' + record.parentId + '</td>' +
                    '</tr>';
            }
        });
    }

}

HTML -

<template>
    <lightning-card>
        <table class="table">
        </table> 
    </lightning-card>

    <p class="p"></p>
</template>

Here we are simply creating rows for the table in the javascript and adding them to the table, simply a dom manipulation. I don't think there is any other approach we can have in the HTML file for the recursion except recursive lwc.

I hope this helps. Please feel free to point out the mistakes or whatever I have done wrong.

2
  • Thank you for your suggestion. I did evaluate manually creating the TRs. Not in the way you outlined, but using the lwc:dom="manual" approach. I ruled it out because the documentation says that styling is not extended to appended nodes and that dom tree creation outside of the LWC rendering process is not recommended.
    – rStrunk
    Apr 13, 2023 at 21:51
  • Thank you for this information :))
    – tejas
    Apr 17, 2023 at 18:35

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .