Tree Select
<oge-tree-select> is the hierarchical counterpart of <oge-select-box>: the same field chrome — label modes, validation subscript, clear button, Signal Forms and reactive forms — with a full oge-tree-view as the popup. It is a WAI-ARIA combobox with aria-haspopup="tree"; opening moves real DOM focus into the tree, so the tree's own APG keyboard map (arrows, Home/End, type-ahead, *) keeps working.
Basic usage
The committed value is the selected node's key. A single click picks a node and closes the popup; the chevron expands, so choosing never fights with browsing. Double-click also expands — expandEvent changes that.
value: null
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTreeSelect } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeTreeSelect],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tree-select
label="Folder"
[items]="folders"
keyExpr="id"
parentIdExpr="parentId"
displayExpr="name"
[rootValue]="null"
[(value)]="folderId"
/>
`,
})
export class Demo {
protected readonly folders = [
{ id: 1, parentId: null, name: 'Documents' },
{ id: 2, parentId: 1, name: 'Reports' },
{ id: 3, parentId: 2, name: 'Q1.pdf' },
];
protected readonly folderId = signal<unknown>(null);
}Nested data & search
Point itemsExpr at the children field for hierarchical payloads. searchEnabled puts the tree's own search box inside the popup — accent-insensitive, auto-expanding to matches and highlighting them.
value: null
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTreeSelect } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeTreeSelect],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- nested payloads need only itemsExpr -->
<oge-tree-select
label="Source file"
[items]="tree"
itemsExpr="children"
displayExpr="name"
[searchEnabled]="true"
[(value)]="fileId"
/>
`,
})
export class Demo {
protected readonly tree = [
{ id: 1, name: 'src', children: [{ id: 2, name: 'main.ts' }] },
];
protected readonly fileId = signal<unknown>(null);
}Multiple selection
With multiple the value becomes an array of keys and the popup stays open while you pick. Checking a node cascades to its descendants and normalizes up, so selectedKeysMode: 'leavesOnly' is often what you actually want to store.
value: (none)
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTreeSelect } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeTreeSelect],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tree-select
label="Permissions"
[items]="folders"
displayExpr="name"
[rootValue]="null"
selectionMode="multiple"
showCheckBoxes="selectAll"
selectedKeysMode="leavesOnly"
[(value)]="permissions"
/>
`,
})
export class Demo {
protected readonly folders = [
{ id: 1, parentId: null, name: 'Documents' },
{ id: 2, parentId: 1, name: 'Reports' },
{ id: 3, parentId: 2, name: 'Q1.pdf' },
];
// checking a node cascades to its descendants; 'leavesOnly' reports
// just the childless keys, so the value stays the concrete grants
protected readonly permissions = signal<unknown>([]);
}Lazy load on demand
Bind only the roots and let loadChildren fetch the rest on first expand; a placeholder row shows while the promise is pending. Fetched nodes join the index, so a cascading selection reaches them too.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTreeSelect } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeTreeSelect],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tree-select
label="Remote folder"
[items]="roots"
displayExpr="name"
[rootValue]="null"
hasItemsExpr="hasItems"
[loadChildren]="loadChildren"
[(value)]="remoteId"
/>
`,
})
export class Demo {
protected readonly roots = [
{ id: 1, parentId: null, name: 'Server root', hasItems: true },
{ id: 2, parentId: null, name: 'readme.txt', hasItems: false },
];
protected readonly remoteId = signal<unknown>(null);
// called once per node, on first expand — a placeholder row shows meanwhile
protected readonly loadChildren = (parent: { id: number; name: string }) =>
new Promise<{ id: number; parentId: number; name: string }[]>((resolve) =>
setTimeout(
() => resolve([{ id: parent.id * 100, parentId: parent.id, name: 'logs' }]),
700,
),
);
}