diff --git a/COMPONENT_INDEX.md b/COMPONENT_INDEX.md
index 3cd4849125..ab5811074e 100644
--- a/COMPONENT_INDEX.md
+++ b/COMPONENT_INDEX.md
@@ -4699,20 +4699,21 @@ export interface TreeNode {
### Props
-| Prop name | Required | Kind | Reactive | Type | Default value | Description |
-| :------------ | :------- | :-------------------- | :------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
-| expandedIds | No | let | Yes | ReadonlyArray | [] | Set the node ids to be expanded |
-| selectedIds | No | let | Yes | ReadonlyArray | [] | Set the node ids to be selected |
-| activeId | No | let | Yes | TreeNodeId | "" | Set the current active node id Only one node can be active |
-| nodes | No | let | No | Array | [] | Provide an array of nodes to render |
-| size | No | let | No | "default" | "compact" | "default" | Specify the TreeView size |
-| labelText | No | let | No | string | "" | Specify the label text |
-| hideLabel | No | let | No | boolean | false | Set to `true` to visually hide the label text |
-| expandAll | No | function | No | () => void | () => { expandedIds = [...nodeIds]; } | Programmatically expand all nodes |
-| collapseAll | No | function | No | () => void | () => { expandedIds = []; } | Programmatically collapse all nodes |
-| expandNodes | No | function | No | (filterId?: (node: TreeNode) => boolean) => void | () => { expandedIds = flattenedNodes .filter( (node) => filterNode(node) || node.nodes?.some((child) => filterNode(child) && child.nodes), ) .map((node) => node.id); } | Programmatically expand a subset of nodes. Expands all nodes if no argument is provided |
-| collapseNodes | No | function | No | (filterId?: (node: TreeNode) => boolean) => void | () => { expandedIds = flattenedNodes .filter((node) => expandedIds.includes(node.id) && !filterNode(node)) .map((node) => node.id); } | Programmatically collapse a subset of nodes. Collapses all nodes if no argument is provided |
-| showNode | No | function | No | (id: TreeNodeId) => void | () => { for (const child of nodes) { const nodes = findNodeById(child, id); if (nodes) { const ids = nodes.map((node) => node.id); const nodeIds = new Set(ids); expandNodes((node) => nodeIds.has(node.id)); const lastId = ids[ids.length - 1]; activeId = lastId; selectedIds = [lastId]; tick().then(() => { ref?.querySelector(\`[id="${lastId}"]\`)?.focus(); }); break; } } } | Programmatically show a node by `id`. The matching node will be expanded, selected, and focused |
+| Prop name | Required | Kind | Reactive | Type | Default value | Description |
+| :------------ | :------- | :-------------------- | :------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
+| expandedIds | No | let | Yes | ReadonlyArray | [] | Set the node ids to be expanded |
+| selectedIds | No | let | Yes | ReadonlyArray | [] | Set the node ids to be selected |
+| activeId | No | let | Yes | TreeNodeId | "" | Set the current active node id Only one node can be active |
+| nodes | No | let | No | Array | [] | Provide a nested array of nodes to render |
+| size | No | let | No | "default" | "compact" | "default" | Specify the TreeView size |
+| labelText | No | let | No | string | "" | Specify the label text |
+| hideLabel | No | let | No | boolean | false | Set to `true` to visually hide the label text |
+| expandAll | No | function | No | () => void | () => { expandedIds = [...nodeIds]; } | Programmatically expand all nodes |
+| collapseAll | No | function | No | () => void | () => { expandedIds = []; } | Programmatically collapse all nodes |
+| toHierarchy | No | function | No | (flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[] | () => { return th(flatArray); } | Create a nested array from a flat array |
+| expandNodes | No | function | No | (filterId?: (node: TreeNode) => boolean) => void | () => { expandedIds = flattenedNodes .filter( (node) => filterNode(node) || node.nodes?.some((child) => filterNode(child) && child.nodes), ) .map((node) => node.id); } | Programmatically expand a subset of nodes. Expands all nodes if no argument is provided |
+| collapseNodes | No | function | No | (filterId?: (node: TreeNode) => boolean) => void | () => { expandedIds = flattenedNodes .filter((node) => expandedIds.includes(node.id) && !filterNode(node)) .map((node) => node.id); } | Programmatically collapse a subset of nodes. Collapses all nodes if no argument is provided |
+| showNode | No | function | No | (id: TreeNodeId) => void | () => { for (const child of nodes) { const nodes = findNodeById(child, id); if (nodes) { const ids = nodes.map((node) => node.id); const nodeIds = new Set(ids); expandNodes((node) => nodeIds.has(node.id)); const lastId = ids[ids.length - 1]; activeId = lastId; selectedIds = [lastId]; tick().then(() => { ref?.querySelector(\`[id="${lastId}"]\`)?.focus(); }); break; } } } | Programmatically show a node by `id`. The matching node will be expanded, selected, and focused |
### Slots
diff --git a/docs/package-lock.json b/docs/package-lock.json
index aa37ed8078..81fdc5494b 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -24,7 +24,7 @@
}
},
"..": {
- "version": "0.86.1",
+ "version": "0.86.2",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
diff --git a/docs/src/COMPONENT_API.json b/docs/src/COMPONENT_API.json
index 469dea2d09..962830d49a 100644
--- a/docs/src/COMPONENT_API.json
+++ b/docs/src/COMPONENT_API.json
@@ -3235,7 +3235,10 @@
"ts": "interface DataTableCell<\n Row = DataTableRow,\n> {\n key:\n | DataTableKey\n | (string & {});\n value: DataTableValue;\n display?: (\n item: DataTableValue,\n row: DataTableRow,\n ) => DataTableValue;\n}\n"
}
],
- "generics": ["Row", "Row extends DataTableRow = DataTableRow"],
+ "generics": [
+ "Row",
+ "Row extends DataTableRow = DataTableRow"
+ ],
"rest_props": {
"type": "Element",
"name": "div"
@@ -17767,7 +17770,7 @@
{
"name": "nodes",
"kind": "let",
- "description": "Provide an array of nodes to render",
+ "description": "Provide a nested array of nodes to render",
"type": "Array",
"value": "[]",
"isFunction": false,
@@ -17872,6 +17875,18 @@
"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",
@@ -18063,4 +18078,4 @@
}
}
]
-}
+}
\ No newline at end of file
diff --git a/docs/src/pages/components/TreeView.svx b/docs/src/pages/components/TreeView.svx
index c7c97c92de..5c639924c2 100644
--- a/docs/src/pages/components/TreeView.svx
+++ b/docs/src/pages/components/TreeView.svx
@@ -64,6 +64,7 @@ Expanded nodes can be set using `expandedIds`.
+
## Initial multiple selected nodes
Initial multiple selected nodes can be set using `selectedIds`.
@@ -107,3 +108,13 @@ Use the `TreeView.showNode` method to show a specific node.
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.
+
+
\ No newline at end of file
diff --git a/docs/src/pages/framed/TreeView/TreeViewFlatArray.svelte b/docs/src/pages/framed/TreeView/TreeViewFlatArray.svelte
new file mode 100644
index 0000000000..0f14cc36ef
--- /dev/null
+++ b/docs/src/pages/framed/TreeView/TreeViewFlatArray.svelte
@@ -0,0 +1,69 @@
+
+
+ console.log("select", detail)}
+ on:toggle={({ detail }) => console.log("toggle", detail)}
+ on:focus={({ detail }) => console.log("focus", detail)}
+/>
+
+
Active node id: {activeId}
+
Selected ids: {JSON.stringify(selectedIds)}
+
+
diff --git a/src/TreeView/TreeView.svelte b/src/TreeView/TreeView.svelte
index 31e3f26795..7f3a028ec6 100644
--- a/src/TreeView/TreeView.svelte
+++ b/src/TreeView/TreeView.svelte
@@ -37,7 +37,7 @@
*/
/**
- * Provide an array of nodes to render
+ * Provide a nested array of nodes to render
* @type {Array}
*/
export let nodes = [];
@@ -89,6 +89,14 @@
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
@@ -147,6 +155,7 @@
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)}`;
diff --git a/src/TreeView/index.d.ts b/src/TreeView/index.d.ts
new file mode 100644
index 0000000000..52e3204e61
--- /dev/null
+++ b/src/TreeView/index.d.ts
@@ -0,0 +1,2 @@
+export { default as TreeView } from "./TreeView.svelte";
+export { toHierarchy } from "./treeview";
diff --git a/src/TreeView/index.js b/src/TreeView/index.js
index 59f96be0f3..52e3204e61 100644
--- a/src/TreeView/index.js
+++ b/src/TreeView/index.js
@@ -1 +1,2 @@
export { default as TreeView } from "./TreeView.svelte";
+export { toHierarchy } from "./treeview";
diff --git a/src/TreeView/treeview.d.ts b/src/TreeView/treeview.d.ts
new file mode 100644
index 0000000000..9506bbdc5e
--- /dev/null
+++ b/src/TreeView/treeview.d.ts
@@ -0,0 +1,9 @@
+import { type TreeNode } from "./TreeView.svelte";
+/**
+ * Create a nested array from a flat array
+ */
+export function toHierarchy(
+ flatArray: TreeNode[] & { pid?: any }[],
+): TreeNode[];
+
+export default toHierarchy;
diff --git a/src/TreeView/treeview.js b/src/TreeView/treeview.js
new file mode 100644
index 0000000000..5804ae92d2
--- /dev/null
+++ b/src/TreeView/treeview.js
@@ -0,0 +1,40 @@
+/**
+ * Create a nested array from a flat array
+ * @type {(flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[]}
+ */
+export function toHierarchy(flatArray) {
+ /** @type TreeNode[] */
+ const tree = [];
+ /** @type TreeNode[] */
+ const childrenOf = [];
+
+ flatArray.forEach((dstItem) => {
+ const { id, pid } = dstItem;
+ childrenOf[id] = childrenOf[id] || [];
+ dstItem["nodes"] = childrenOf[id];
+
+ if (pid) {
+ // objects without pid are root level objects.
+ childrenOf[pid] = childrenOf[pid] || [];
+ delete dstItem.pid; // TreeNode type doesn't have pid.
+ childrenOf[pid].push(dstItem);
+ } else {
+ delete dstItem.pid;
+ tree.push(dstItem);
+ }
+ });
+
+ // Remove the empty nodes props that make TreeView render a twistie.
+ function removeEmptyNodes(element) {
+ element.forEach((elmt) => {
+ if (elmt.nodes?.length === 0) delete elmt.nodes;
+ else {
+ removeEmptyNodes(elmt.nodes);
+ }
+ });
+ }
+ removeEmptyNodes(tree);
+ return tree;
+}
+
+export default toHierarchy;
diff --git a/src/index.js b/src/index.js
index ea1a84cf46..75a2c8dcc9 100644
--- a/src/index.js
+++ b/src/index.js
@@ -127,6 +127,7 @@ 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 {
diff --git a/tests/TreeView/TreeView.test.ts b/tests/TreeView/TreeView.test.ts
index b2a0a6a3f2..0e29a9d9c4 100644
--- a/tests/TreeView/TreeView.test.ts
+++ b/tests/TreeView/TreeView.test.ts
@@ -1,6 +1,7 @@
import { render, screen } from "@testing-library/svelte";
import { user } from "../setup-tests";
import TreeView from "./TreeView.test.svelte";
+import TreeViewFlatArray from "./TreeViewFlatArray.test.svelte";
describe("TreeView", () => {
const getItemByName = (name: RegExp) => {
@@ -37,4 +38,14 @@ describe("TreeView", () => {
text: "AI / Machine learning",
});
});
+
+ it("can turn flat array into nested array", async () => {
+ const consoleLog = vi.spyOn(console, "log");
+ render(TreeViewFlatArray);
+ const firstItem = getItemByName(/Blockchain/);
+ expect(firstItem).toBeInTheDocument();
+ await user.click(firstItem);
+ expect(getSelectedItemByName(/Blockchain/)).toBeInTheDocument();
+ expect(consoleLog).toBeCalledWith("selectedIds", [7]);
+ });
});
diff --git a/tests/TreeView/TreeViewFlatArray.test.svelte b/tests/TreeView/TreeViewFlatArray.test.svelte
new file mode 100644
index 0000000000..005394724b
--- /dev/null
+++ b/tests/TreeView/TreeViewFlatArray.test.svelte
@@ -0,0 +1,77 @@
+
+
+ console.log("select", detail)}
+ on:toggle={({ detail }) => console.log("toggle", detail)}
+ on:focus={({ detail }) => console.log("focus", detail)}
+ let:node
+>
+ {node.text}
+
+
+
diff --git a/types/TreeView/TreeView.svelte.d.ts b/types/TreeView/TreeView.svelte.d.ts
index 342f24c95b..b70f211992 100644
--- a/types/TreeView/TreeView.svelte.d.ts
+++ b/types/TreeView/TreeView.svelte.d.ts
@@ -15,7 +15,7 @@ type $RestProps = SvelteHTMLElements["ul"];
type $Props = {
/**
- * Provide an array of nodes to render
+ * Provide a nested array of nodes to render
* @default []
*/
nodes?: Array;
@@ -94,6 +94,11 @@ export default class TreeView extends SvelteComponentTyped<
*/
collapseAll: () => void;
+ /**
+ * Create a nested array from a flat array
+ */
+ toHierarchy: (flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[];
+
/**
* Programmatically expand a subset of nodes.
* Expands all nodes if no argument is provided
diff --git a/types/TreeView/index.d.ts b/types/TreeView/index.d.ts
new file mode 100644
index 0000000000..52e3204e61
--- /dev/null
+++ b/types/TreeView/index.d.ts
@@ -0,0 +1,2 @@
+export { default as TreeView } from "./TreeView.svelte";
+export { toHierarchy } from "./treeview";
diff --git a/types/TreeView/treeview.d.ts b/types/TreeView/treeview.d.ts
new file mode 100644
index 0000000000..9506bbdc5e
--- /dev/null
+++ b/types/TreeView/treeview.d.ts
@@ -0,0 +1,9 @@
+import { type TreeNode } from "./TreeView.svelte";
+/**
+ * Create a nested array from a flat array
+ */
+export function toHierarchy(
+ flatArray: TreeNode[] & { pid?: any }[],
+): TreeNode[];
+
+export default toHierarchy;
diff --git a/types/index.d.ts b/types/index.d.ts
index 1174ce26c1..d48b80fcb6 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -143,6 +143,7 @@ export { default as TooltipFooter } from "./Tooltip/TooltipFooter.svelte";
export { default as TooltipDefinition } from "./TooltipDefinition/TooltipDefinition.svelte";
export { default as TooltipIcon } from "./TooltipIcon/TooltipIcon.svelte";
export { default as TreeView } from "./TreeView/TreeView.svelte";
+export { default as toHierarchy } from "./TreeView/treeview";
export { default as Truncate } from "./Truncate/Truncate.svelte";
export { default as truncate } from "./Truncate/truncate";
export { default as Header } from "./UIShell/Header.svelte";