Skip to content

Commit

Permalink
Re-work toHierarchy utility
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
metonym and bhavers committed Dec 9, 2024
1 parent 651779d commit 5f1e8de
Show file tree
Hide file tree
Showing 29 changed files with 414 additions and 273 deletions.
29 changes: 14 additions & 15 deletions COMPONENT_INDEX.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 1 addition & 13 deletions docs/src/COMPONENT_API.json
Original file line number Diff line number Diff line change
Expand Up @@ -17770,7 +17770,7 @@
{
"name": "nodes",
"kind": "let",
"description": "Provide a nested array of nodes to render",
"description": "Provide an array of nodes to render",
"type": "Array<TreeNode>",
"value": "[]",
"isFunction": false,
Expand Down Expand Up @@ -17875,18 +17875,6 @@
"constant": false,
"reactive": false
},
{
"name": "toHierarchy",
"kind": "function",
"description": "Create a nested array from a flat array",
"type": "(flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[]",
"value": "() => {\n return th(flatArray);\n}",
"isFunction": true,
"isFunctionDeclaration": true,
"isRequired": false,
"constant": false,
"reactive": false
},
{
"name": "expandNodes",
"kind": "function",
Expand Down
9 changes: 8 additions & 1 deletion docs/src/pages/components/RecursiveList.svx
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,11 @@ Set `type` to `"ordered"` to use the ordered list variant.

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

<FileSource src="/framed/RecursiveList/RecursiveListOrderedNative" />
<FileSource src="/framed/RecursiveList/RecursiveListOrderedNative" />

## Flat data structure

If working with a flat data structure, use the `toHierarchy` utility
to convert a flat data structure into a hierarchical array accepted by the `nodes` prop.

<FileSource src="/framed/RecursiveList/RecursiveListFlatArray" />
10 changes: 3 additions & 7 deletions docs/src/pages/components/TreeView.svx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ Expanded nodes can be set using `expandedIds`.

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


## Initial multiple selected nodes

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

## Flat data structure

Use the `toHierarchy` method to provide a flat data structure to the `nodes` property.

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

<FileSource src="/framed/TreeView/TreeViewFlatArray" />
<FileSource src="/framed/TreeView/TreeViewFlatArray" />
20 changes: 20 additions & 0 deletions docs/src/pages/framed/RecursiveList/RecursiveListFlatArray.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script>
import { RecursiveList, toHierarchy } from "carbon-components-svelte";
const nodesFlat = [
{ id: 1, text: "Item 1" },
{ id: 2, text: "Item 1a", pid: 1 },
{ id: 3, html: "<h5>HTML content</h5>", pid: 2 },
{ id: 4, text: "Item 2" },
{ id: 5, href: "https://svelte.dev/", pid: 4 },
{
id: 6,
href: "https://svelte.dev/",
text: "Link with custom text",
pid: 4,
},
{ id: 7, text: "Item 3" },
];
</script>

<RecursiveList nodes={toHierarchy(nodesFlat, (node) => node.pid)} />
75 changes: 17 additions & 58 deletions docs/src/pages/framed/TreeView/TreeViewFlatArray.svelte
Original file line number Diff line number Diff line change
@@ -1,69 +1,28 @@
<script>
import { TreeView, toHierarchy } from "carbon-components-svelte";
import WatsonMachineLearning from "carbon-icons-svelte/lib/WatsonMachineLearning.svelte";
import Analytics from "carbon-icons-svelte/lib/Analytics.svelte";
import Blockchain from "carbon-icons-svelte/lib/Blockchain.svelte";
import DataBase from "carbon-icons-svelte/lib/DataBase.svelte";
import SignalStrength from "carbon-icons-svelte/lib/SignalStrength.svelte";
let activeId = "";
let selectedIds = [];
let nodesFlat = [
{ id: 0, text: "AI / Machine learning", icon: WatsonMachineLearning },
{ id: 1, text: "Analytics", icon: Analytics },
{ id: 2, text: "IBM Analytics Engine", pid: 1, icon: Analytics },
{ id: 3, text: "Apache Spark", pid: 2, icon: Analytics },
{ id: 4, text: "Hadoop", icon: Analytics, pid: 2 },
{ id: 5, text: "IBM Cloud SQL Query", icon: Analytics, pid: 1 },
{ id: 6, text: "IBM Db2 Warehouse on Cloud", icon: Analytics, pid: 1 },
{ id: 7, text: "Blockchain", icon: Blockchain },
{ id: 8, text: "IBM Blockchain Platform", icon: Blockchain, pid: 7 },
{ id: 9, text: "Databases", icon: DataBase },
{
id: 10,
text: "IBM Cloud Databases for Elasticsearch",
icon: DataBase,
pid: 9,
},
{
id: 11,
text: "IBM Cloud Databases for Enterprise DB",
icon: DataBase,
pid: 9,
},
{ id: 12, text: "IBM Cloud Databases for MongoDB", icon: DataBase, pid: 9 },
{
id: 13,
text: "IBM Cloud Databases for PostgreSQL",
icon: DataBase,
pid: 9,
},
{ id: 14, text: "Integration", icon: SignalStrength, disabled: true },
{
id: 15,
text: "IBM API Connect",
icon: SignalStrength,
disabled: true,
pid: 14,
},
{ id: 0, text: "AI / Machine learning", icon: Analytics },
{ id: 1, text: "Analytics" },
{ id: 2, text: "IBM Analytics Engine", pid: 1 },
{ id: 3, text: "Apache Spark", pid: 2 },
{ id: 4, text: "Hadoop", pid: 2 },
{ id: 5, text: "IBM Cloud SQL Query", pid: 1 },
{ id: 6, text: "IBM Db2 Warehouse on Cloud", pid: 1 },
{ id: 7, text: "Blockchain" },
{ id: 8, text: "IBM Blockchain Platform", pid: 7 },
{ id: 9, text: "Databases" },
{ id: 10, text: "IBM Cloud Databases for Elasticsearch", pid: 9 },
{ id: 11, text: "IBM Cloud Databases for Enterprise DB", pid: 9 },
{ id: 12, text: "IBM Cloud Databases for MongoDB", pid: 9 },
{ id: 13, text: "IBM Cloud Databases for PostgreSQL", pid: 9 },
{ id: 14, text: "Integration", disabled: true },
{ id: 15, text: "IBM API Connect", disabled: true, pid: 14 },
];
</script>

<TreeView
labelText="Cloud Products"
nodes={toHierarchy(nodesFlat)}
bind:activeId
bind:selectedIds
on:select={({ detail }) => console.log("select", detail)}
on:toggle={({ detail }) => console.log("toggle", detail)}
on:focus={({ detail }) => console.log("focus", detail)}
nodes={toHierarchy(nodesFlat, (node) => node.pid)}
/>

<div>Active node id: {activeId}</div>
<div>Selected ids: {JSON.stringify(selectedIds)}</div>

<style>
div {
margin-top: var(--cds-spacing-05);
}
</style>
11 changes: 1 addition & 10 deletions src/TreeView/TreeView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
*/
/**
* Provide a nested array of nodes to render
* Provide an array of nodes to render
* @type {Array<TreeNode>}
*/
export let nodes = [];
Expand Down Expand Up @@ -89,14 +89,6 @@
expandedIds = [];
}
/**
* Create a nested array from a flat array
* @type {(flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[]}
*/
export function toHierarchy(flatArray) {
return th(flatArray);
}
/**
* Programmatically expand a subset of nodes.
* Expands all nodes if no argument is provided
Expand Down Expand Up @@ -155,7 +147,6 @@
import { createEventDispatcher, setContext, onMount, tick } from "svelte";
import { writable } from "svelte/store";
import TreeViewNodeList from "./TreeViewNodeList.svelte";
import { toHierarchy as th } from "./treeview";
const dispatch = createEventDispatcher();
const labelId = `label-${Math.random().toString(36)}`;
Expand Down
1 change: 0 additions & 1 deletion src/TreeView/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { default as TreeView } from "./TreeView.svelte";
export { toHierarchy } from "./treeview";
1 change: 0 additions & 1 deletion src/TreeView/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { default as TreeView } from "./TreeView.svelte";
export { toHierarchy } from "./treeview";
9 changes: 0 additions & 9 deletions src/TreeView/treeview.d.ts

This file was deleted.

40 changes: 0 additions & 40 deletions src/TreeView/treeview.js

This file was deleted.

2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ export { Tooltip, TooltipFooter } from "./Tooltip";
export { TooltipDefinition } from "./TooltipDefinition";
export { TooltipIcon } from "./TooltipIcon";
export { TreeView } from "./TreeView";
export { toHierarchy } from "./TreeView/treeview";
export { Truncate } from "./Truncate";
export { default as truncate } from "./Truncate/truncate";
export {
Expand All @@ -153,3 +152,4 @@ export {
HeaderSearch,
} from "./UIShell";
export { UnorderedList } from "./UnorderedList";
export { toHierarchy } from "./utils/toHierarchy";
21 changes: 21 additions & 0 deletions src/utils/toHierarchy.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type NodeLike = {
id: string | number;
nodes?: NodeLike[];
[key: string]: any;
};

/** Create a hierarchical tree from a flat array. */
export function toHierarchy<
T extends NodeLike,
K extends keyof Omit<T, "id" | "nodes">,
>(
flatArray: T[] | readonly T[],
/**
* Function that returns the parent ID for a given node.
* @example
* toHierarchy(flatArray, (node) => node.parentId);
*/
getParentId: (node: T) => T[K] | null,
): (T & { nodes?: (T & { nodes?: T[] })[] })[];

export default toHierarchy;
49 changes: 49 additions & 0 deletions src/utils/toHierarchy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// @ts-check
/**
* Create a nested array from a flat array.
* @typedef {Object} NodeLike
* @property {string | number} id - Unique identifier for the node
* @property {NodeLike[]} [nodes] - Optional array of child nodes
* @property {Record<string, any>} [additionalProperties] - Any additional properties
*
* @param {NodeLike[]} flatArray - Array of flat nodes to convert
* @param {function(NodeLike): (string|number|null)} getParentId - Function to get parent ID for a node
* @returns {NodeLike[]} Hierarchical tree structure
*/
export function toHierarchy(flatArray, getParentId) {
/** @type {NodeLike[]} */
const tree = [];
const childrenOf = new Map();
const itemsMap = new Map(flatArray.map((item) => [item.id, item]));

flatArray.forEach((item) => {
const parentId = getParentId(item);

// Only create nodes array if we have children.
const children = childrenOf.get(item.id);
if (children) {
item.nodes = children;
}

// Check if parentId exists using Map instead of array lookup.
const parentExists = parentId && itemsMap.has(parentId);

if (parentId && parentExists) {
if (!childrenOf.has(parentId)) {
childrenOf.set(parentId, []);
}
childrenOf.get(parentId).push(item);

const parent = itemsMap.get(parentId);
if (parent) {
parent.nodes = childrenOf.get(parentId);
}
} else {
tree.push(item);
}
});

return tree;
}

export default toHierarchy;
6 changes: 6 additions & 0 deletions tests/App.test.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { TreeView as TreeViewNav } from "carbon-components-svelte";
import TreeView from "./TreeView/TreeView.test.svelte";
import TreeViewHierarchy from "./TreeView/TreeView.hierarchy.test.svelte";
import { onMount } from "svelte";
const routes = [
Expand All @@ -9,6 +10,11 @@
name: "TreeView",
component: TreeView,
},
{
path: "/treeview-hierarchy",
name: "TreeViewHierarchy",
component: TreeViewHierarchy,
},
] as const;
let currentPath = window.location.pathname;
Expand Down
Loading

0 comments on commit 5f1e8de

Please sign in to comment.