Nested category tree
Product categories, a menu, a taxonomy, a chart of accounts: these are usually stored as a table with a parent_id column, an adjacency list. Klad takes those rows as they are and draws a category tree. People can browse it, search it and reorganize it with drag and drop, and you do not need to build a recursive component or a nested JSON structure.
From a parent_id table
SELECT id, parent_id, name FROM categories;import { createKlad } from '@klad/core'
const rows = await fetch('/api/categories').then((r) => r.json())
const chart = createKlad(document.getElementById('categories')!, {
data: rows.map((row) => ({
id: String(row.id),
parentId: row.parent_id == null ? null : String(row.parent_id),
name: row.name,
})),
layout: 'file', // an indented category list; 'tidy' for a top-down chart
nodeSize: { w: 320, h: 30 },
toggleOnNodeClick: true,
dragAndDrop: true,
label: (item) => String(item.name ?? ''),
})Every category with no parent becomes a root, so a forest of top-level categories works as it is. The order inside each parent is the order of the rows.
Moving a category
With dragAndDrop: true, dropping a category on another makes it a subcategory. Dropping it before or after another moves it among its siblings. The event fires before anything moves, so your server can still refuse:
chart.on('nodeDrop', async (event) => {
event.preventDefault()
const ok = await fetch('/api/categories/move', {
method: 'POST',
body: JSON.stringify({ ids: event.ids, parentId: event.parentId, index: event.index }),
}).then((r) => r.ok)
if (ok) chart.api.update(applyMove(rows, event)) // your own data update
})A category can never be dropped into its own subcategories, so a move cannot create a cycle. See Drag and drop for drop modes, keyboard moves and multi-select.
Large catalogues
- Load subcategories on demand. Send only the top level and fetch each branch when it is opened. See Children on demand.
- Filter to the matching categories and the parents above them. See Filtering.
- Cap categories with thousands of children. See Very wide levels.
- Subtree queries.
chart.api.stats(id)returns nested-setlftandrgtbounds, so "is this category under that one" is a comparison rather than a walk up the tree. See the Chart API.
Your own row
Each category is your own Vue, React or DOM component, with a product count, a visibility toggle, an edit button or whatever you need. See Node content. The same data also draws as a tree view, an org chart, or a sunburst that sizes each category by its number of products.