Tree CRUD
Status: ✅ Implemented — gridLayout() = GridLayout.tree + @SplitCrud
Intent
Section titled “Intent”Navigate a hierarchy and edit any node without leaving the tree.
Problem
Section titled “Problem”Some data is naturally a tree: categories inside categories, an org chart, a folder/document structure, a bill of materials, a project breakdown. A flat, paginated table cannot express it — pagination shreds the parent/child relationship, and forcing the user to drill down screen by screen (open category → open subcategory → open product → back → back) is the CRUD Tunnel anti-pattern applied to a tree, paid once per level.
Solution
Section titled “Solution”Render the master list of a CRUD as an expandable tree grid, and pair it with a split view so selecting a node opens its editor in place.
Two ingredients:
- rows that carry a self-referential
childrencollection; gridLayout()returnsGridLayout.tree, on a@SplitCrudCRUD.
The row
Section titled “The row”public record CategoryRow(
@Priority(value = 1, identifier = true) // becomes the expandable tree column String name,
int products,
List<CategoryRow> children // the branches) {}- The first / identifier column becomes the tree column — it carries the expand/collapse toggle.
- A row with a non-empty
childrenlist gets an expand toggle; leaf rows leavechildrenempty ornull. search()returns only the root rows — each branch expands lazily from itschildrenlist.
Homogeneous tree — one editor for every node
Section titled “Homogeneous tree — one editor for every node”When every node is the same kind of thing (a category that nests into categories, a folder inside a folder), a plain AutoCrud is enough — just switch the layout to tree:
@UI("/catalog")@SplitCrudpublic class CatalogTree extends AutoCrud<Category> {
@Override public GridLayout gridLayout() { return GridLayout.tree; }}Category is Identifiable, exposes a List<Category> children, and its repository returns the root categories (each carrying its nested children). Selecting any node opens the Category editor in the detail pane; saving refreshes the tree in place.
Heterogeneous tree — a different editor per node
Section titled “Heterogeneous tree — a different editor per node”Real workspaces mix node types: a project holds services, a service holds modules, a module holds entities — and each edits with its own form. Drop to the lower-level Crud<…> and supply the tree through a CrudAdapter:
- a single row record describes every node (
label, a hiddenid,children); search()builds the whole tree once;save()/getEditor(id)route to the right editor by decoding the node id;- grouping nodes (a folder, a category header) set
viewable = falseso they show no open button and own no editor — only their children are openable.
@UI("/workspace")@SplitCrud @NotCreatable @NotDeletablepublic class WorkspaceCrud extends Crud< CrudEditorForm<String>, CrudEditorForm<String>, CrudCreationForm<String>, NoFilters, WorkspaceRow, String> {
private final WorkspaceAdapter adapter; // supplies the tree + the per-node editors
@Override public GridLayout gridLayout() { return GridLayout.tree; } @Override public boolean searchable() { return true; }
@Override public Object save(HttpRequest req) { String id = req.getString(getIdFieldForRow()); ((CrudEditorForm<?>) adapter.getEditor(id, req)).save(req); // route by id return id; }}public record WorkspaceRow(
String label, // the tree column
@Hidden String id, // real id for openable nodes; synthetic for grouping nodes
@Hidden boolean viewable, // false → grouping node: no open button, no editor
List<WorkspaceRow> children) {}The adapter’s search() returns the roots with their children nested; getEditor(id) and getView(id) inspect the id to return the correct concept’s form. See CRUD interfaces for the CrudAdapter contract.
Structure
Section titled “Structure”┌──────────────────────────┬──────────────────────┐│ ▾ Electronics │ ││ ▾ Phones │ Product: iPhone ││ iPhone ◀ selected│ Name __________ ││ Pixel │ Price __________ ││ ▸ Laptops │ ││ ▸ Home & Garden │ [Save] [Delete] │└──────────────────────────┴──────────────────────┘Tree is opt-in — the flat layouts are not
Section titled “Tree is opt-in — the flat layouts are not”For a non-hierarchical listing you never choose the layout: Mateu auto-selects table, list, or cards from the column weights and the available screen width (a few narrow columns → table, an identifier plus secondary info → list, image/rich content or many columns → cards). See Listing layout for the full algorithm.
tree is the one layout that is never auto-selected. You opt into it with gridLayout() precisely because it changes the data contract — the rows must carry a self-referential children list.
Principles served
Section titled “Principles served”- Preserve context — the whole hierarchy stays visible and interactive while you edit a node.
- Minimize navigation — no drill-down/back trips; expand and edit in place.
- Workflow over screens — the tree is the workspace, not a stop on the way to one.
When to use it
Section titled “When to use it”- Data that is genuinely a tree: catalogs, org charts, folder/document structures, BOMs, project breakdowns.
- Not for flat collections — a table / list / cards CRUD (optionally
@SplitCrud) is simpler and needs nochildrenlist.
Related
Section titled “Related”- Split View — the flat (non-hierarchical) master-detail CRUD this pattern builds on.
- Listing layout — how the flat layouts are auto-selected, and every
GridLayoutvalue. - Workspace — composing several heterogeneous panels on one screen.