Skip to content

Commit 48afd18

Browse files
metonymbhavers
andauthored
feat: add toHierarchy utility for TreeView, RecursiveList (#2072)
Co-authored-by: Bram <[email protected]>
1 parent f1a27ec commit 48afd18

19 files changed

+413
-23
lines changed

docs/src/pages/components/RecursiveList.svx

Lines changed: 8 additions & 1 deletion
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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,10 @@ Use the `TreeView.showNode` method to show a specific node.
107107
If a matching node is found, it will be expanded, selected, and focused.
108108

109109
<FileSource src="/framed/TreeView/TreeViewShowNode" />
110+
111+
## Flat data structure
112+
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.
115+
116+
<FileSource src="/framed/TreeView/TreeViewFlatArray" />
Lines changed: 20 additions & 0 deletions
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)} />
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script>
2+
import { TreeView, toHierarchy } from "carbon-components-svelte";
3+
import Analytics from "carbon-icons-svelte/lib/Analytics.svelte";
4+
5+
let nodesFlat = [
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 },
22+
];
23+
</script>
24+
25+
<TreeView
26+
labelText="Cloud Products"
27+
nodes={toHierarchy(nodesFlat, (node) => node.pid)}
28+
/>

src/TreeView/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as TreeView } from "./TreeView.svelte";

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,4 @@ export {
152152
HeaderSearch,
153153
} from "./UIShell";
154154
export { UnorderedList } from "./UnorderedList";
155+
export { toHierarchy } from "./utils/toHierarchy";

src/utils/toHierarchy.d.ts

Lines changed: 21 additions & 0 deletions
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

Lines changed: 49 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script lang="ts">
2+
import { RecursiveList } from "carbon-components-svelte";
3+
import toHierarchy from "../../src/utils/toHierarchy";
4+
5+
let nodes = toHierarchy(
6+
[
7+
{ id: 1, text: "Item 1" },
8+
{ id: 2, text: "Item 1a", pid: 1 },
9+
{ id: 3, html: "<h5>HTML content</h5>", pid: 2 },
10+
{ id: 4, text: "Item 2" },
11+
{ id: 5, href: "https://svelte.dev/", pid: 4 },
12+
{
13+
id: 6,
14+
href: "https://svelte.dev/",
15+
text: "Link with custom text",
16+
pid: 4,
17+
},
18+
{ id: 7, text: "Item 3" },
19+
],
20+
(node) => node.pid,
21+
);
22+
</script>
23+
24+
<RecursiveList type="ordered" {nodes} />

0 commit comments

Comments
 (0)