Tree / tree-grid view plugin for Object UI.
Renders a self-referencing object as an indented, expand/collapse tree-grid — the right view for hierarchies of unbounded depth such as business unit / org chart, category trees, menu trees, BOMs, or nested comments. (Grouping handles fixed-depth hierarchies; a tree handles arbitrary depth.)
It registers one component type via the ComponentRegistry:
object-tree— the object-bound renderer, and the one an author selects
The bare tree registry key (view:tree), a second key on the same renderer,
is RETIRED (objectui#10859 batch 8): objectui validate refused it at type,
and nothing authored it. tree survives as a host-composition VIEW type (see
below), which ObjectView and ListView compose into an object-tree node.
Author an object-tree node. ObjectQLComponentSchema narrows on type, so
each key is checked against ObjectTreeSchema, which mirrors the object-tree
row of @objectstack/spec (17.5.0 and later):
import type { ObjectQLComponentSchema } from '@object-ui/types';
const schema: ObjectQLComponentSchema = {
type: 'object-tree',
objectName: 'business_unit',
tree: {
parentField: 'parent', // single-parent pointer (auto-detected if omitted)
labelField: 'name', // indented first column
fields: ['manager'], // additional flat columns
defaultExpandedDepth: 1, // 0 = roots only; omit = expand all
},
filter: [{ field: 'status', operator: 'equals', value: 'active' }],
navigation: { mode: 'drawer' },
};| Key | Description |
|---|---|
objectName |
The object to query. One of data, staticData and objectName is the record source, read in that order; objectui validate refuses a node with none of the three. |
data |
A { provider, … } configuration, read first. { provider: 'value', items } draws those records and { provider: 'object', object } queries that object. The api and schema providers draw no rows on the tree. A bare array is not a record source (objectui#8348): inline rows go under staticData. |
staticData |
Inline records, read second: a data configuration wins, and a tree carrying both staticData and objectName draws these rows and never queries the object. |
tree |
The field configuration — the spec's TreeConfig (table below). |
filter |
Rule array [{ field, operator, value }], lowered to $filter and applied to inline rows as well. Context tokens such as {current_user_id} are resolved first. A record whose parent the filter removed is drawn as a root. |
navigation |
What a row click opens (below). |
The node's dataSource binding is not a record source on this block: the
registration is not wrapped in ElementDataSourceGate, so nothing lands a
binding's object on objectName, and objectui validate does not count it.
| Key | Default | Description |
|---|---|---|
parentField |
auto-detected | Field holding the parent reference. When omitted, the renderer picks the object's tree field (or a lookup/master_detail that references the same object). |
labelField |
name |
Field rendered indented in the first column. |
fields |
[] |
Additional fields rendered as flat columns after the label. |
defaultExpandedDepth |
unset | Initial expansion depth. 0 = roots only; unset = expand everything. |
Records whose parent is missing (or points outside the result set) are kept as roots, so nothing is silently dropped.
tree block, so author that.
navigation takes the spec's NavigationConfig ({ mode, size, width, openNewTab, preventNavigation }), the block a list view declares. On a tree no
parent view navigates for:
- Absent: a click opens nothing. This renderer supplies no drawer default.
drawer,modalandpopoveropen the row's record in that overlay;splitopens it beside the tree, which stays drawn.new_windowopens/{objectName}/record/{id}in a new tab;noneopens nothing.page, and a block withoutmode(it takes the spec'spagedefault), open a record page through the record navigator the host publishes (the console publishes one on its custom pages, record pages and list views). The object isdata.objectwhendatais the object provider, so it wins when both are written, and the tree'sobjectNameotherwise. Under a host that publishes none, such as an embedded renderer, or on a tree that names neither (inline rows with noobjectName), there is no record page to open and the click opens nothing.preventNavigation: trueopens nothing whatever the mode.openNewTab: trueopens the record page in a new tab and outranks every mode exceptnone.sizepicks the overlay width bucket; the deprecatedwidthwins over it.
A click handler from a parent view outranks the whole key.
defaultExpandedDepth seeds expansion; it does not own it. The seeded set is
derived from the forest during render rather than mirrored into component state,
so a tree that expands by default is painted expanded in the first commit that
has rows — there is no frame in which the forest is drawn collapsed
(objectui#8666).
When the record set changes — a refetch, a filter, a host that reallocates the rows — the seed is recomputed for the new forest. A node the user opened or closed by clicking its chevron, and which is still in the forest, keeps the user's answer; every other node, a genuinely new one included, takes the seed. Expansion is per-mount session state: it is not addressable and is not persisted.
tree is not an authorable view type. Neither ObjectViewSchema.defaultViewType
nor NamedListView.type admits it — both are the same seven-value union that stops
at map — so no authored document selects a tree view, and ObjectViewSchema
declares no views member at all. The tree branch runs only when a host
composes ObjectView with a views prop, whose entries carry id and label
and are typed ViewType. That was ruled deliberate on objectui#5321
(2026-08-20): tree and chart are recorded as host-composition-only surfaces
rather than added to the authored unions.
The live consumer is the console: it passes stored view records to ObjectView
as views, and its create-view dialog offers tree among the types a console
user can create. To render a tree from authored metadata, write the
object-tree node above.
Host config is not untyped config. A block a host stores and re-writes is a
contract, so the per-view tree block is exported from @object-ui/types and
the renderer imports it rather than keeping a private copy (objectui#8253,
ruled 2026-09-07).
@objectstack/spec owns this block — it
declares it as TreeConfig and hangs it on ListView.tree — and
@object-ui/types already publishes it a second way, derived, as
ListViewSchema['tree']. TreeViewConfig is now a derivation of the
protocol's block rather than a copy of it — in packages/types/src/views.ts
it is a one-line alias of NonNullable<ListView['tree']>, taken from
@objectstack/spec/ui. So the accurate claim is the narrower one: this is the
name a host writes against, and it tracks the protocol by construction rather
than by anyone remembering to update it.
import type { TreeViewConfig } from '@object-ui/types';
// The block a host writes as `tree` on a `views` entry, or as `options.tree`
// on a stored view record. Every key is optional; a host writes the subset it
// means.
const treeConfig: TreeViewConfig = {
parentField: 'parent', // single-parent pointer (auto-detected if omitted)
labelField: 'name', // indented first column
fields: ['name', 'manager'], // additional flat columns
defaultExpandedDepth: 1, // 0 = roots only; omit = expand all
};Annotating the block is what turns a typo into a diagnostic: parentFeild used
to be stored, read by nobody and reported by nothing, because the views entry
admits any key. Against this type it is a compile error.
⛔ titleField is not part of this block. objectui#8253 declared it as a
legacy second rung for labelField; @objectstack/spec@17.4.0 refuses
tree.titleField by name (TreeConfigSchema is strict since spec #15469), so
declaring it published a key the protocol rejects — an author who followed this
type was refused at publish. objectui#8841 removed it.
The renderers still tolerate a titleField already stored on a view record:
plugin-view, plugin-list and the console's own composition each fall back to
it when labelField is absent, so nothing that renders today stops rendering.
Those reads are untyped tolerance awaiting a follow-up, ⛔ not a declaration —
write labelField, which is the protocol's spelling and wins wherever both are
present.
⛔ This does not make tree an authorable view type. objectui#5321 is
unchanged: the block is written by a host, never by a document author, and
the authored node is the object-tree schema at the top of this file.
MIT — see LICENSE.