# Tree Operations

This page demonstrates common tree operations: adding folders and files, renaming, deleting, and expanding/collapsing nodes.

```tsx
import {
  createModel,
  findTreeNode,
  removeTreeNodes,
  updateTree,
} from "cx/data";
import { Controller, expr, KeySelection, TreeAdapter } from "cx/ui";
import { Button, Grid, TreeNode } from "cx/widgets";

import "../../icons/lucide";

interface TreeRecord {
  id: number;
  name: string;
  $leaf?: boolean;
  $expanded?: boolean;
  $level?: number;
  $children?: TreeRecord[];
}

interface PageModel {
  data: TreeRecord[];
  selection: number;
  $record: TreeRecord;
}

const m = createModel<PageModel>();

let idSeq = 100;

class PageController extends Controller {
  onInit() {
    this.store.set(m.data, [
      {
        id: 1,
        name: "Documents",
        $leaf: false,
        $expanded: true,
        $children: [
          { id: 2, name: "report.pdf", $leaf: true },
          { id: 3, name: "notes.txt", $leaf: true },
        ],
      },
      {
        id: 4,
        name: "Images",
        $leaf: false,
        $children: [
          { id: 5, name: "photo.jpg", $leaf: true },
          { id: 6, name: "logo.png", $leaf: true },
        ],
      },
    ]);
  }

  expandAll() {
    this.store.update(
      m.data,
      (data) =>
        updateTree(
          data,
          (node) => ({ ...node, $expanded: true }),
          (node) => !node.$leaf,
          "$children",
        ) || data,
    );
  }

  collapseAll() {
    this.store.update(
      m.data,
      (data) =>
        updateTree(
          data,
          (node) => ({ ...node, $expanded: false }),
          (node) => !node.$leaf,
          "$children",
        ) || data,
    );
  }

  addFolder() {
    this.addNode(false);
  }

  addFile() {
    this.addNode(true);
  }

  addNode(leaf: boolean) {
    const selectedId = this.store.get(m.selection);
    const data = this.store.get(m.data);

    const newNode: TreeRecord = leaf
      ? { id: ++idSeq, name: `file-${idSeq}.txt`, $leaf: true }
      : { id: ++idSeq, name: `Folder ${idSeq}`, $leaf: false, $children: [] };

    if (!selectedId) {
      // Add to root
      this.store.update(m.data, (data) => [...data, newNode]);
    } else {
      // Check if selected node is a folder
      const selectedNode = findTreeNode(
        data,
        (n) => n.id === selectedId,
        "$children",
      );
      if (selectedNode && selectedNode.$leaf) {
        alert("Cannot add to a file. Please select a folder.");
        return;
      }
      // Add to selected folder
      this.store.update(
        m.data,
        (data) =>
          updateTree(
            data,
            (node) => ({
              ...node,
              $expanded: true,
              $children: [...(node.$children || []), newNode],
            }),
            (node) => node.id === selectedId,
            "$children",
          ) || data,
      );
    }
  }

  deleteSelected() {
    const selectedId = this.store.get(m.selection);
    if (!selectedId) return;

    this.store.update(
      m.data,
      (data) =>
        removeTreeNodes(data, (node) => node.id === selectedId, "$children") ||
        data,
    );
    this.store.delete(m.selection);
  }

  renameSelected() {
    const selectedId = this.store.get(m.selection);
    if (!selectedId) return;

    const newName = prompt("Enter new name:");
    if (!newName) return;

    this.store.update(
      m.data,
      (data) =>
        updateTree(
          data,
          (node) => ({ ...node, name: newName }),
          (node) => node.id === selectedId,
          "$children",
        ) || data,
    );
  }
}

export default (
  <div controller={PageController}>
    <div style="margin-bottom: 16px; display: flex; gap: 8px; flex-wrap: wrap">
      <Button
        onClick={(e, instance) =>
          instance.getControllerByType(PageController).addFolder()
        }
        text="Add Folder"
        icon="folder"
      />
      <Button
        onClick={(e, instance) =>
          instance.getControllerByType(PageController).addFile()
        }
        text="Add File"
        icon="file"
      />
      <Button
        onClick={(e, instance) =>
          instance.getControllerByType(PageController).renameSelected()
        }
        text="Rename"
        icon="pencil"
        disabled={expr(m.selection, (s) => !s)}
      />
      <Button
        onClick={(e, instance) =>
          instance.getControllerByType(PageController).deleteSelected()
        }
        text="Delete"
        icon="trash"
        disabled={expr(m.selection, (s) => !s)}
      />
      <Button
        onClick={(e, instance) =>
          instance.getControllerByType(PageController).expandAll()
        }
        text="Expand All"
      />
      <Button
        onClick={(e, instance) =>
          instance.getControllerByType(PageController).collapseAll()
        }
        text="Collapse All"
      />
    </div>
    <Grid
      records={m.data}
      mod="tree"
      style="height: 300px"
      scrollable
      keyField="id"
      dataAdapter={{ type: TreeAdapter }}
      selection={{ type: KeySelection, bind: m.selection, keyField: "id" }}
      columns={[
        {
          header: "Name",
          field: "name",
          children: (
            <TreeNode
              expanded={m.$record.$expanded}
              leaf={m.$record.$leaf}
              level={m.$record.$level}
              text={m.$record.name}
            />
          ),
        },
      ]}
    />
  </div>
);

```

Select a folder to add files or subfolders inside it. Select any node to rename or delete it.

## Adding Nodes

To add a child node, use [updateTree](/docs/utilities/update-tree) to find the parent and append to its `$children` array:

```tsx
updateTree(
  data,
  (node) => ({
    ...node,
    $expanded: true,
    $children: [...(node.$children || []), newNode],
  }),
  (node) => node.id === parentId,
  "$children"
);
```

Before adding, use [findTreeNode](/docs/utilities/find-tree-node) to check if the selected node is a folder:

```tsx
const selectedNode = findTreeNode(data, (n) => n.id === selectedId, "$children");
if (selectedNode?.$leaf) {
  alert("Cannot add to a file. Please select a folder.");
  return;
}
```

## Removing Nodes

Use [removeTreeNodes](/docs/utilities/remove-tree-nodes) to delete nodes by ID:

```tsx
removeTreeNodes(data, (node) => node.id === targetId, "$children");
```

This removes the node and all its descendants.

## Renaming Nodes

Use [updateTree](/docs/utilities/update-tree) to find and update a node:

```tsx
updateTree(
  data,
  (node) => ({ ...node, name: newName }),
  (node) => node.id === targetId,
  "$children"
);
```

## Expanding and Collapsing

To expand or collapse all folders, use [updateTree](/docs/utilities/update-tree) with a predicate that matches non-leaf nodes:

```tsx
// Expand all
updateTree(
  data,
  (node) => ({ ...node, $expanded: true }),
  (node) => !node.$leaf,
  "$children"
);

// Collapse all
updateTree(
  data,
  (node) => ({ ...node, $expanded: false }),
  (node) => !node.$leaf,
  "$children"
);
```

## Preserving State Across Reloads

When tree data is reloaded from a server, expanded state is normally lost. To preserve it:

```tsx
<Grid
  records={m.data}
  keyField="id"
  dataAdapter={{
    type: TreeAdapter,
    restoreExpandedNodesOnLoad: true,
  }}
  columns={columns}
/>
```

The `keyField` is required so nodes can be matched across data updates.

See also: [updateTree](/docs/utilities/update-tree), [findTreeNode](/docs/utilities/find-tree-node), [removeTreeNodes](/docs/utilities/remove-tree-nodes), [Tree Grid](/docs/tables/tree-grid), [TreeAdapter](/docs/tables/tree-adapter)