This context provides a step-by-step guide on building a TreeTable using Ant Design System in a React project.
Abstract
The guide begins with setting up a working environment using Create React App, followed by installing necessary packages such as antd, styled-components, and unique-names-generator. The process of building a TreeTable is broken down into ten steps, starting with creating a flat table and gradually adding features like expanding parent nodes, highlighting newly added children, and implementing "Add a child" and "Remove the node" actions. The guide also covers adding angled lines for TreeTable children, displaying the total count, and implementing "Open All" and "Collapse All" buttons. The final step involves breaking down the code into different files for better organization and separation of concerns.
Opinions
The author emphasizes the importance of using antd's Table component to build a TreeTable.
The author suggests using styled-components for styling the TreeTable.
The author recommends using unique-names-generator to generate unique and memorable name strings for the TreeTable content.
The author highlights the need to manage the expandedRowKeys state to control whether tree nodes are expanded.
The author suggests using the scrollIntoView method to scroll the newly added child into view.
The author recommends separating the long App.js file into distinct sections, where each section addresses a separate concern.
The author provides a complete example in a GitHub repository and a live demo using storybook.
A Table, also called a data grid, is an arrangement of data in rows and columns or possibly in a more complex structure. It is an essential building block of a user interface.
A TreeTable is an extension of a Table that supports a tree-like hierarchy, typically in the first column. As a Tree, the hierarchy is determined by the parent-children relationships.
We have written about Ant Tables with sorting, filtering, pagination, row selections, infinite scrolling, and many more features. In this article, we will build a TreeTable using its nested-table capability. It takes ten steps to build, and you can always scroll down to the end to see the final source code.
Set up Working Environment
We use Create React App as a base to explore TreeTables. The following command creates a React project:
% yarn create react-app react-tree-table% cd react-tree-table
antd: Ant Design System is an open source code for enterprise-level UI design languages and React UI library. We use antd’s Table component to build a TreeTable.
styled-components: It is a React-specific CSS-in-JS styling solution that writes CSS code in JavaScript to style components. We use styled-components to style the TreeTable.
unique-names-generator: It is a tool to generate unique and memorable name strings. We use unique-names-generator to generate the TreeTable content.
After the installation, these packages become part of dependencies in package.json:
At line 40, the initial Table is created with ten rows.
At lines 42–64, Table columns are defined.
At lines 67–71, the app returns a Table component, with pagination turned off (line 68), columns (line 69) and dataSource (line 70) set.
Execute yarn start, and we see a flat Table.
Step 2: Implement Add a Child Action
To have a TreeTable, we need to implement Add a child action. A child node is created under the current row when the cell is clicked.
Here is the modified Table columns:
At lines 14–25, the onCell attribute is added, which sets props for the cell. One of props can be the onClick callback (lines 16–23). It also passes in the current row’s record as a parameter (line 14).
In antd, a Table row is a parent if its record’s children prop is an array of records (could be an empty array for dynamic loading).
At line 18, record.children is set to [], if it is not initialized yet.
At line 21, the newly generated row is added to record.children. As we have mutated tableData by changing record.children, setTableData is called to update the reference.
Execute yarn start. Click the first row’s Add a child, and we see a + icon shows up. It indicates that Tomato Outdoor Impala is a parent.
Image by author
Click the first row’s Add a child again, and nothing happens. The newly added children are hidden behind the + icon.
We would like a parent node expanded to make the newly added row visible. This leads to the next step.
Step 3: Expand the Active Parent Node
How do we expand a parent node?
Table has an expandable prop that controls which parents are open. expandable is a subtable, and could even have an independent column header. For a TreeTable, the subtable’s column header is not needed. Instead, the following configuration props are relevant:
defaultExpandAllRows: It specifies whether to expand all rows initially, and the default value is false.
defaultExpandedRowKeys: It specifies initial expanded row keys, and its type is string[].
expandedRowClassName: It specifies the expanded row’s className, and its type is function(record, index, indent): string.
expandedRowKeys: It specifies the row keys whose node is open, and its type is string[].
expandIcon: It customizes the row expansion icon, and its type is function({ expanded, onExpand, record })): ReactNode.
indentSize: It specifies the indentation size for a subtree, and its type is number.
onExpand: It is a callback function that is invoked when the row expansion icon is clicked. Its type is function(expanded, record).
onExpandedRowsChange: It is a callback function that is invoked when the expanded rows change. Its type is function(expandedRows).
In TreeTable, expandedRowKeys is used to control whether tree nodes are expanded. The rows that have keys in expandedRowKeys are expanded. Otherwise, those rows are collapsed. Since we take control of expandedRowKeys, its value needs to be managed whenever a user clicks a row expand icon.
At line 8, the state, expandedRowKeys, is created.
At line 32, the newly added child’s parent key is added to expandedRowKeys.
At lines 52–57, Table’s expandable prop is defined.
Execute yarn start. Whenever we add a child, the parent node is automatically expanded.
Image by author
We can see added children. However, after adding several children, we get confused about which one is the newly added one. The TreeTable needs further improvement.
Step 4: Highlight the Newly Added Child
There are many ways to style a row. For simplicity, we use Table’s rowSelection to highlight the newly added row. If Table’s rowSelection prop is configured, the selected row is highlighted. Among many props of rowSelection, we set two of them:
selectedRowKeys: It specifies the selected row keys and its type is string[].
type: It specifies the type, either checkbox or radio. The default value is checkbox.
We choose radio for rowSelection, as radio does not allow multi-selection.
rowSelection={{ selectedRowKeys, type: 'radio' }}
This is purely a hack. Alternatively, we can use className to highlight the row. The className can be conditionally set based on whether its key is equal to newRowKey that will be defined in the next step.
Here is the modified src/App.js:
At line 9, the state, selectedRowKeys, is created.
At line 34, the newly created child’s key is added to selectedRowKeys.
At line 52, Table’s rowSelection prop is defined.
At line 59, clear the highlighting when a user clicks a row expansion icon.
Execute yarn start. The newly added child is highlighted.
Image by author
If we keep adding more children, eventually, the newly added child is out of view.
Image by author
We need another improvement.
Step 5: Scroll the Newly Added Child Into the View
A JavaScript element has a method, scrollIntoView(), that scrolls its ancestor’s container to make the element viewable.
It has three formats:
scrollIntoView(): It takes no parameter and scrolls to the top of the ancestor’s container.
scrollIntoView(alignToTop): alignToTop is a boolean value. If true, it scrolls to the top of the ancestor’s container. Otherwise, it scrolls to the bottom of the ancestor’s container.
scrollIntoView(scrollIntoViewOptions): scrollIntoViewOptions is an Object with the following properties:
– behavior: It defines the transition animation, either auto or smooth, and the default value is auto.
– block: It defines vertical alignment, either start, center, end, or nearest, and the default value is start.
– inline: It defines horizontal alignment, either start, center, end, or nearest, and the default value is nearest.
scrollIntoView(true) is equivalent to scrollIntoViewOptions({block: "start", inline: "nearest"}). scrollIntoView(false) is equivalent to scrollIntoViewOptions({block: "end", inline: "nearest"}).
With scrollIntoView, the newly added child can be scrolled into view.
Here is the modified src/App.js:
At line 9, the state, newRowKey, is created.
At lines 12–19, useEffect is invoked when newRowKey is changed. It finds the newly added row element (lines 14–16) and calls scrollIntoView (line 17) to scroll it into view.
At line 44, newRowKey is updated with the newly added child’s key.
At line 71, clear newRowKey upon row expansion changes.
Execute yarn start. Keep clicking Add a child on the row, Olive Personal Owl, and the newly added child is always in view.
Image by author
Step 6: Implement Remove the Node Action
We have Add a child working. The next step is to make Remove the node working.
removeRowFromTableData is a method that removes a row with the specific key from the input list, data. There are two use cases for this method:
If a row does not have a parent, the row is removed from the table data. In this case, data is tableData.
If a row has a parent, the row is removed from its parent’s children list. In this case, data is record.parent.children.
constremoveRowFromTableData = (data = [], key) => {
if (key) {
const index = data.findIndex((item) => item.key === key);
if (index !== -1) {
data.splice(index, 1);
}
}
};
Here is the modified Table columns:
To know record.parent, the Add column onCell’s onClick callback (lines 14–29) saves a parent reference for the newly added child (line 21).
At lines 36–55, the Remove column adds the onCell attribute. Its callback, onClick, handles the remove action. If record has a parent, it is removed from the parent’s children list (line 41). Otherwise, it is removed from tableData (line 46).
At lines 42–44, it deletes an empty children. This removes the parent node’s expansion icon.
At lines 48–52, the component’s states are updated:
– tableData is updated with a new reference (line 48).
– expandedRowKeySet is updated by deleting record.key (lines 49–50).
– newRowKey is updated to undefined (line 51).
– selectedRowKeys is updated to [] (line 52).
Execute yarn start. Click Remove the node on a row, and the row, along with its children, if any, will be removed.
Step 7: Implement Angled Lines for TreeTable Children
Here is a TreeTable with multiple parent nodes expanded.
Image by author
When the list is long, it may not be straightforward to associate a parent with their children. We want to add angled lines that point from a parent to their children.
Here is the icon, EnterOutlined.
Image by author
We flip it along the x-axis to show the association from a parent to each of its children.
Image by author
Here is src/App.js with angled line changes:
At lines 8–22, a Container is defined to host the table at line 60. Container ensures the content does not wrap. The overflow part will be hidden and show an ellipse if it does not have enough space.
At lines 24–33, the icon, EnterOutlined, is flipped along the x-axis to be an angled line.
At lines 46–51, render is added to the Name column. If record has a parent, FlippedEnterIcon is prepended to the name (line 48).
Execute yarn start. The parent-children relationship looks clearer with the angled lines.
Image by author
Step 8: Implement Total Count for TreeTable
With a TreeTable partial expanded, it is hard to tell how many rows (nodes) there are. It would help if the total count is displayed.
At lines 29–31, it styles an action button to have a 10px spacing at the left side.
At lines 184–206, footer includes the Open All button (lines 187–195) and the Collapse All button (lines 196–204).
Execute yarn start. The Open All and Collapse All buttons are available at footer, along with the total count.
Image by author
Step 10: Break Down into Different Files
The above TreeTable code works, but it violates the design principle, separation of concerns. We should separate the long src/App.js into distinct sections, where each section addresses a separate concern.
We break down the above src/App.js into three files.
src
├── App.js
├── TreeTable.js
└── utils.js
src/App.js: It is an application that uses the TreeTable component.
src/utils.js: It is a util collection that provides helper methods.
This article has described how to set up storybook to show components, as well as how to put it online via GitHub Pages.
Conclusion
It takes ten steps to build a TreeTable using Ant Design System’s nested-table capability. The TreeTable can add children under the current row. The parent node will be automatically opened upon adding a child. The newly added child is highlighted (a hack from the radio selection) and scrolled into view. Deleting a row will delete the row along with all children if any.
The TreeTable footer shows the nodes’ total count, whether these tree nodes are open or collapsed. The footer also has buttons to Open All and Collapse All all tree nodes.
Thanks for reading.
Thanks, Elaine Lee, S Sreeram, Sushmitha Aitha, Pendri Laxmi Prasanna, and Siddhartha Chinthapally, for working with me on TreeTable.
Want to Connect?
If you are interested, check out my directory of web development articles.