Skip to content

Commit 5f1e8de

Browse files
metonymbhavers
andcommitted
Re-work toHierarchy utility
Refactor `toHiearchy` to be more generic, performant - Use callback to "pick" generic parent ID property instead of requiring that `pid` be hardcoded` - Account for edge cases of an invalid parent ID - Use Map to store node children for lookups - Use one pass instead of removing empty nodes at the very end - DX: use generics to type `toHierarchy` - Make `toHierarchy` even more generic (reusable with `RecursiveList`) Co-Authored-By: Bram <[email protected]>
1 parent 651779d commit 5f1e8de

29 files changed

+414
-273
lines changed

COMPONENT_INDEX.md

+14-15
Large diffs are not rendered by default.

docs/package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/src/COMPONENT_API.json

+1-13
Original file line numberDiff line numberDiff line change
@@ -17770,7 +17770,7 @@
1777017770
{
1777117771
"name": "nodes",
1777217772
"kind": "let",
17773-
"description": "Provide a nested array of nodes to render",
17773+
"description": "Provide an array of nodes to render",
1777417774
"type": "Array<TreeNode>",
1777517775
"value": "[]",
1777617776
"isFunction": false,
@@ -17875,18 +17875,6 @@
1787517875
"constant": false,
1787617876
"reactive": false
1787717877
},
17878-
{
17879-
"name": "toHierarchy",
17880-
"kind": "function",
17881-
"description": "Create a nested array from a flat array",
17882-
"type": "(flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[]",
17883-
"value": "() => {\n return th(flatArray);\n}",
17884-
"isFunction": true,
17885-
"isFunctionDeclaration": true,
17886-
"isRequired": false,
17887-
"constant": false,
17888-
"reactive": false
17889-
},
1789017878
{
1789117879
"name": "expandNodes",
1789217880
"kind": "function",

docs/src/pages/components/RecursiveList.svx

+8-1
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,11 @@ Set `type` to `"ordered"` to use the ordered list variant.
3737

3838
Set `type` to `"ordered-native"` to use the native styles for an ordered list.
3939

40-
<FileSource src="/framed/RecursiveList/RecursiveListOrderedNative" />
40+
<FileSource src="/framed/RecursiveList/RecursiveListOrderedNative" />
41+
42+
## Flat data structure
43+
44+
If working with a flat data structure, use the `toHierarchy` utility
45+
to convert a flat data structure into a hierarchical array accepted by the `nodes` prop.
46+
47+
<FileSource src="/framed/RecursiveList/RecursiveListFlatArray" />

docs/src/pages/components/TreeView.svx

+3-7
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ Expanded nodes can be set using `expandedIds`.
6464

6565
<FileSource src="/framed/TreeView/TreeViewExpanded" />
6666

67-
6867
## Initial multiple selected nodes
6968

7069
Initial multiple selected nodes can be set using `selectedIds`.
@@ -111,10 +110,7 @@ If a matching node is found, it will be expanded, selected, and focused.
111110

112111
## Flat data structure
113112

114-
Use the `toHierarchy` method to provide a flat data structure to the `nodes` property.
115-
116-
This method will transform a flat array of objects into the hierarchical array as expected by `nodes`.
117-
The child objects in the flat array need to have a `pid` property to reference its parent.
118-
When `pid` is not provided the object is assumed to be at the root of the tree.
113+
If working with a flat data structure, use the `toHierarchy` utility
114+
to convert a flat data structure into a hierarchical array accepted by the `nodes` prop.
119115

120-
<FileSource src="/framed/TreeView/TreeViewFlatArray" />
116+
<FileSource src="/framed/TreeView/TreeViewFlatArray" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script>
2+
import { RecursiveList, toHierarchy } from "carbon-components-svelte";
3+
4+
const nodesFlat = [
5+
{ id: 1, text: "Item 1" },
6+
{ id: 2, text: "Item 1a", pid: 1 },
7+
{ id: 3, html: "<h5>HTML content</h5>", pid: 2 },
8+
{ id: 4, text: "Item 2" },
9+
{ id: 5, href: "https://svelte.dev/", pid: 4 },
10+
{
11+
id: 6,
12+
href: "https://svelte.dev/",
13+
text: "Link with custom text",
14+
pid: 4,
15+
},
16+
{ id: 7, text: "Item 3" },
17+
];
18+
</script>
19+
20+
<RecursiveList nodes={toHierarchy(nodesFlat, (node) => node.pid)} />
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,28 @@
11
<script>
22
import { TreeView, toHierarchy } from "carbon-components-svelte";
3-
import WatsonMachineLearning from "carbon-icons-svelte/lib/WatsonMachineLearning.svelte";
43
import Analytics from "carbon-icons-svelte/lib/Analytics.svelte";
5-
import Blockchain from "carbon-icons-svelte/lib/Blockchain.svelte";
6-
import DataBase from "carbon-icons-svelte/lib/DataBase.svelte";
7-
import SignalStrength from "carbon-icons-svelte/lib/SignalStrength.svelte";
84
9-
let activeId = "";
10-
let selectedIds = [];
115
let nodesFlat = [
12-
{ id: 0, text: "AI / Machine learning", icon: WatsonMachineLearning },
13-
{ id: 1, text: "Analytics", icon: Analytics },
14-
{ id: 2, text: "IBM Analytics Engine", pid: 1, icon: Analytics },
15-
{ id: 3, text: "Apache Spark", pid: 2, icon: Analytics },
16-
{ id: 4, text: "Hadoop", icon: Analytics, pid: 2 },
17-
{ id: 5, text: "IBM Cloud SQL Query", icon: Analytics, pid: 1 },
18-
{ id: 6, text: "IBM Db2 Warehouse on Cloud", icon: Analytics, pid: 1 },
19-
{ id: 7, text: "Blockchain", icon: Blockchain },
20-
{ id: 8, text: "IBM Blockchain Platform", icon: Blockchain, pid: 7 },
21-
{ id: 9, text: "Databases", icon: DataBase },
22-
{
23-
id: 10,
24-
text: "IBM Cloud Databases for Elasticsearch",
25-
icon: DataBase,
26-
pid: 9,
27-
},
28-
{
29-
id: 11,
30-
text: "IBM Cloud Databases for Enterprise DB",
31-
icon: DataBase,
32-
pid: 9,
33-
},
34-
{ id: 12, text: "IBM Cloud Databases for MongoDB", icon: DataBase, pid: 9 },
35-
{
36-
id: 13,
37-
text: "IBM Cloud Databases for PostgreSQL",
38-
icon: DataBase,
39-
pid: 9,
40-
},
41-
{ id: 14, text: "Integration", icon: SignalStrength, disabled: true },
42-
{
43-
id: 15,
44-
text: "IBM API Connect",
45-
icon: SignalStrength,
46-
disabled: true,
47-
pid: 14,
48-
},
6+
{ id: 0, text: "AI / Machine learning", icon: Analytics },
7+
{ id: 1, text: "Analytics" },
8+
{ id: 2, text: "IBM Analytics Engine", pid: 1 },
9+
{ id: 3, text: "Apache Spark", pid: 2 },
10+
{ id: 4, text: "Hadoop", pid: 2 },
11+
{ id: 5, text: "IBM Cloud SQL Query", pid: 1 },
12+
{ id: 6, text: "IBM Db2 Warehouse on Cloud", pid: 1 },
13+
{ id: 7, text: "Blockchain" },
14+
{ id: 8, text: "IBM Blockchain Platform", pid: 7 },
15+
{ id: 9, text: "Databases" },
16+
{ id: 10, text: "IBM Cloud Databases for Elasticsearch", pid: 9 },
17+
{ id: 11, text: "IBM Cloud Databases for Enterprise DB", pid: 9 },
18+
{ id: 12, text: "IBM Cloud Databases for MongoDB", pid: 9 },
19+
{ id: 13, text: "IBM Cloud Databases for PostgreSQL", pid: 9 },
20+
{ id: 14, text: "Integration", disabled: true },
21+
{ id: 15, text: "IBM API Connect", disabled: true, pid: 14 },
4922
];
5023
</script>
5124

5225
<TreeView
5326
labelText="Cloud Products"
54-
nodes={toHierarchy(nodesFlat)}
55-
bind:activeId
56-
bind:selectedIds
57-
on:select={({ detail }) => console.log("select", detail)}
58-
on:toggle={({ detail }) => console.log("toggle", detail)}
59-
on:focus={({ detail }) => console.log("focus", detail)}
27+
nodes={toHierarchy(nodesFlat, (node) => node.pid)}
6028
/>
61-
62-
<div>Active node id: {activeId}</div>
63-
<div>Selected ids: {JSON.stringify(selectedIds)}</div>
64-
65-
<style>
66-
div {
67-
margin-top: var(--cds-spacing-05);
68-
}
69-
</style>

src/TreeView/TreeView.svelte

+1-10
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
*/
3838
3939
/**
40-
* Provide a nested array of nodes to render
40+
* Provide an array of nodes to render
4141
* @type {Array<TreeNode>}
4242
*/
4343
export let nodes = [];
@@ -89,14 +89,6 @@
8989
expandedIds = [];
9090
}
9191
92-
/**
93-
* Create a nested array from a flat array
94-
* @type {(flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[]}
95-
*/
96-
export function toHierarchy(flatArray) {
97-
return th(flatArray);
98-
}
99-
10092
/**
10193
* Programmatically expand a subset of nodes.
10294
* Expands all nodes if no argument is provided
@@ -155,7 +147,6 @@
155147
import { createEventDispatcher, setContext, onMount, tick } from "svelte";
156148
import { writable } from "svelte/store";
157149
import TreeViewNodeList from "./TreeViewNodeList.svelte";
158-
import { toHierarchy as th } from "./treeview";
159150
160151
const dispatch = createEventDispatcher();
161152
const labelId = `label-${Math.random().toString(36)}`;

src/TreeView/index.d.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export { default as TreeView } from "./TreeView.svelte";
2-
export { toHierarchy } from "./treeview";

src/TreeView/index.js

-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export { default as TreeView } from "./TreeView.svelte";
2-
export { toHierarchy } from "./treeview";

src/TreeView/treeview.d.ts

-9
This file was deleted.

src/TreeView/treeview.js

-40
This file was deleted.

src/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ export { Tooltip, TooltipFooter } from "./Tooltip";
127127
export { TooltipDefinition } from "./TooltipDefinition";
128128
export { TooltipIcon } from "./TooltipIcon";
129129
export { TreeView } from "./TreeView";
130-
export { toHierarchy } from "./TreeView/treeview";
131130
export { Truncate } from "./Truncate";
132131
export { default as truncate } from "./Truncate/truncate";
133132
export {
@@ -153,3 +152,4 @@ export {
153152
HeaderSearch,
154153
} from "./UIShell";
155154
export { UnorderedList } from "./UnorderedList";
155+
export { toHierarchy } from "./utils/toHierarchy";

src/utils/toHierarchy.d.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
type NodeLike = {
2+
id: string | number;
3+
nodes?: NodeLike[];
4+
[key: string]: any;
5+
};
6+
7+
/** Create a hierarchical tree from a flat array. */
8+
export function toHierarchy<
9+
T extends NodeLike,
10+
K extends keyof Omit<T, "id" | "nodes">,
11+
>(
12+
flatArray: T[] | readonly T[],
13+
/**
14+
* Function that returns the parent ID for a given node.
15+
* @example
16+
* toHierarchy(flatArray, (node) => node.parentId);
17+
*/
18+
getParentId: (node: T) => T[K] | null,
19+
): (T & { nodes?: (T & { nodes?: T[] })[] })[];
20+
21+
export default toHierarchy;

src/utils/toHierarchy.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// @ts-check
2+
/**
3+
* Create a nested array from a flat array.
4+
* @typedef {Object} NodeLike
5+
* @property {string | number} id - Unique identifier for the node
6+
* @property {NodeLike[]} [nodes] - Optional array of child nodes
7+
* @property {Record<string, any>} [additionalProperties] - Any additional properties
8+
*
9+
* @param {NodeLike[]} flatArray - Array of flat nodes to convert
10+
* @param {function(NodeLike): (string|number|null)} getParentId - Function to get parent ID for a node
11+
* @returns {NodeLike[]} Hierarchical tree structure
12+
*/
13+
export function toHierarchy(flatArray, getParentId) {
14+
/** @type {NodeLike[]} */
15+
const tree = [];
16+
const childrenOf = new Map();
17+
const itemsMap = new Map(flatArray.map((item) => [item.id, item]));
18+
19+
flatArray.forEach((item) => {
20+
const parentId = getParentId(item);
21+
22+
// Only create nodes array if we have children.
23+
const children = childrenOf.get(item.id);
24+
if (children) {
25+
item.nodes = children;
26+
}
27+
28+
// Check if parentId exists using Map instead of array lookup.
29+
const parentExists = parentId && itemsMap.has(parentId);
30+
31+
if (parentId && parentExists) {
32+
if (!childrenOf.has(parentId)) {
33+
childrenOf.set(parentId, []);
34+
}
35+
childrenOf.get(parentId).push(item);
36+
37+
const parent = itemsMap.get(parentId);
38+
if (parent) {
39+
parent.nodes = childrenOf.get(parentId);
40+
}
41+
} else {
42+
tree.push(item);
43+
}
44+
});
45+
46+
return tree;
47+
}
48+
49+
export default toHierarchy;

tests/App.test.svelte

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { TreeView as TreeViewNav } from "carbon-components-svelte";
33
import TreeView from "./TreeView/TreeView.test.svelte";
4+
import TreeViewHierarchy from "./TreeView/TreeView.hierarchy.test.svelte";
45
import { onMount } from "svelte";
56
67
const routes = [
@@ -9,6 +10,11 @@
910
name: "TreeView",
1011
component: TreeView,
1112
},
13+
{
14+
path: "/treeview-hierarchy",
15+
name: "TreeViewHierarchy",
16+
component: TreeViewHierarchy,
17+
},
1218
] as const;
1319
1420
let currentPath = window.location.pathname;

0 commit comments

Comments
 (0)