CxJS

Tree Operations

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

<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>
Name
Documents
report.pdf
notes.txt
Images

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 to find the parent and append to its $children array:

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

Before adding, use findTreeNode to check if the selected node is a folder:

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 to delete nodes by ID:

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

This removes the node and all its descendants.

Renaming Nodes

Use updateTree to find and update a node:

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

Expanding and Collapsing

To expand or collapse all folders, use updateTree with a predicate that matches non-leaf nodes:

// 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:

<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, findTreeNode, removeTreeNodes, Tree Grid, TreeAdapter