Features
Hierarchy & Nested Categories
Collections can define hierarchical (nested) relationships using the hierarchy configuration. This enables tree navigation in the admin listing, breadcrumb navigation in the document editor, and path resolution helpers for consuming apps.
Configuration
Add hierarchy to your collection definition with a self-referencing parent field:
import { f, defineVextroCollection } from "vextro";
export const categories = defineVextroCollection({
label: "Categories",
tableName: "categories",
hierarchy: {
parentField: "parent",
},
fields: {
title: f.text({ required: true }),
parent: f.id("categories"),
slug: f.slug({ sourceField: "title" }),
description: f.textarea(),
},
}); Options
| Option | Type | Default | Description |
|---|---|---|---|
parentField | string | "parent" | Field name that holds the parent document reference. Must be an f.id() field referencing the same table. |
maxDepth | number | 10 | Maximum nesting depth. Prevents runaway hierarchies and protects against circular references. |
Admin UI Behavior
Tree View
When a collection has hierarchy configured, the listing page defaults to a tree layout. Documents are shown as an indented tree with expand/collapse toggles.
- Root documents (no parent) appear at the top level
- Click the chevron to expand/collapse children
- Use "Expand All" / "Collapse All" in the toolbar
- Toggle between tree, list, and grid layouts using the layout switcher
Breadcrumbs
When editing a document in a hierarchical collection, breadcrumbs appear above the document title showing the full ancestry path:
Root Category > Parent Category > Current Category Each ancestor is clickable and links to its edit page.
Safety Features
Circular Reference Prevention
Vextro automatically prevents circular references when setting a document's parent:
- A document cannot be its own parent
- A document cannot be set as a child of one of its own descendants
- If the maximum nesting depth would be exceeded, the save is rejected
These checks run automatically via beforeChange hooks registered when hierarchy is configured.
Orphan Prevention
Deleting a document that has children is blocked by default. You must move or delete the children first. This prevents orphaned documents in the tree.
Path Resolution
Use the resolveFullPath utility to build human-readable paths:
import { resolveFullPath } from "vextro/hierarchy";
// Build a map of all documents
const allDocs = await ctx.db.query("categories").collect();
const docsMap = new Map(allDocs.map((d) => [d._id, d]));
const path = resolveFullPath(
documentId,
"parent", // parentField
"title", // titleField
docsMap,
10, // maxDepth
" > " // separator
);
// "Climbing > Harnesses > Full-body"
The resolveFullPath function operates on an in-memory document map. For large collections, consider caching the map or computing paths on write via afterChange hooks.
Additional Utilities
The vextro/hierarchy subpath also exports these helpers:
| Export | Description |
|---|---|
buildTree(documents, parentField) | Builds a TreeNode[] tree from a flat document array |
resolveAncestors(docId, parentField, docsMap, maxDepth) | Returns ancestor chain from root to immediate parent |
collectDescendantIds(docId, documents, parentField) | Collects all descendant IDs (useful for exclusion filters) |
resolveHierarchyConfig(config) | Resolves raw config with defaults applied |
Best Practices
- Use for category-sized collections (up to ~1000 documents). The tree view loads all documents at once.
- Set
useAsTitleon hierarchy collections so tree items have meaningful labels. - Keep
maxDepthreasonable (5-15 levels). Deeply nested hierarchies are hard to navigate. - Combine with slugs for URL-friendly category paths in your consuming app.