Skip to content

Tree View

The tree view renders a hierarchical structure from a flat list of records. It uses an id/parentId approach — you return all nodes as a flat array and the UI assembles the tree client-side.

Preview
Engineering
Frontend
Alice Johnson
Bob Martinez
Backend
Design
Spec
typescript
new TreeView()
  .endpoint({ method: 'GET', url: '/departments' })
  .idField('id')
  .parentField('parentId')
  .labelField('name')
  .metadata({ title: 'Department Structure' })
  .build()

How it works

The SPA navigates to #/{resource}/tree and fetches a TreeSpec from GET /api/ui/{resource}/tree. The spec describes how to fetch the flat node list and which fields to use for assembling the hierarchy.

Then the SPA fetches all nodes from spec.endpoint and builds the tree by matching each node's parentField value to another node's idField value. Root nodes are those with a null or missing parent.

Basic setup (JS)

typescript
import { TreeView } from '@retrofit-ui/builder-zod';

// Each row must have: id, parentId (or null for roots), name
// URL prefix below is arbitrary — pick anything and match `apiBase` to it.
app.get('/files/departments/tree', (_req, res) => {
  res.json(
    new TreeView()
      .endpoint({ method: 'GET', url: '/departments' })
      .idField('id')
      .parentField('parentId')
      .labelField('name')
      .metadata({ title: 'Department Structure' })
      .build(),
  );
});

Your /departments endpoint returns a flat array:

json
[
  { "id": 1, "name": "Engineering", "parentId": null },
  { "id": 2, "name": "Frontend", "parentId": 1 },
  { "id": 3, "name": "Alice Johnson", "parentId": 2 },
  { "id": 4, "name": "Bob Martinez", "parentId": 2 },
  { "id": 5, "name": "Backend", "parentId": 1 },
  { "id": 6, "name": "Design", "parentId": null }
]

Spec fields

FieldTypeRequiredDescription
endpointEndpointDirectiveFetches the flat node array
idFieldstringField on each node that uniquely identifies it (default: 'id')
parentFieldstringField referencing the parent's ID; null/missing = root (default: 'parentId')
labelFieldstringField to display as the node label (default: 'name')
selection'single' | 'multiple' | 'leaf'Selection mode (default: 'single')
actions{ create?, update?, delete? }CRUD buttons
metadata.titlestringHeading above the tree

Selection modes

Selection modes
'single'
One node at a time. Edit and Delete buttons require exactly one selection.
'multiple'
Multiple nodes. Useful for bulk delete. Edit is disabled when more than one node is selected.
'leaf'
Only leaf nodes (no children) can be selected — enforced by the Shoelace tree component.
Spec
typescript
new TreeView()
  .endpoint({ method: 'GET', url: '/employees' })
  .idField('id')
  .parentField('managerId')
  .labelField('name')
  .selection('multiple')
  .build();

CRUD actions

Add .create(), .update(), and .delete() to enable action buttons:

typescript
new TreeView()
  .endpoint({ method: 'GET', url: '/departments' })
  .idField('id')
  .parentField('parentId')
  .labelField('name')
  .create({ method: 'POST', url: '/departments' })
  .update({ method: 'PUT', url: '/departments/{id}' })
  .delete({ method: 'DELETE', url: '/departments/{id}' })
  .build();
ActionButtonBehaviour
createNew (top-right)Navigates to #/{resource}/new
updateEditNavigates to #/{resource}/{selectedId}; disabled when ≠ 1 item selected
deleteDeleteOpens a confirmation dialog, then calls the endpoint for each selected ID

The delete action is the only one handled inline — it prompts with a confirmation dialog before calling the endpoint, then shows a success or error toast.

Linking from a table

Use a table row action to navigate users to the tree view:

typescript
TableView.schema(ProjectSchema)
  .rowAction({ label: 'Structure', routePattern: '/tree' })
  // note: routes to #/{resource}/tree, not per-entity
  .list({ method: 'GET', url: '/projects' })
  .build();