Select Box
<oge-select-box> is a drop-down select on the shared field chrome: pick one item from a list, optionally filter it by typing, and bind the committed value with signals, Signal Forms or reactive forms. The popup follows the anchor on scroll, flips when cramped and matches the field width.
Basic usage
Bind an array of strings and [(value)] — no mapping needed. Open with the mouse, ↓, Enter or by typing a letter (type-ahead).
nullimport { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSelectBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeSelectBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-select-box
label="City"
[items]="cities"
[(value)]="city"
/>
`,
})
export class Demo {
protected readonly cities = ['Ankara', 'Berlin', 'Lisbon', 'Oslo', 'Tokyo'];
protected readonly city = signal<unknown>(null);
}Data mapping & search
Objects map through displayExpr/valueExpr (field name or function). searchEnabled turns the input editable and filters client-side; searchChanged + [loading] are the server-side escape hatch.
nullimport { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSelectBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeSelectBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-select-box
label="Assignee"
[items]="users"
displayExpr="name"
valueExpr="id"
[searchEnabled]="true"
[showClearButton]="true"
[(value)]="assigneeId"
(searchChanged)="onSearch($event.text)"
/>
`,
})
export class Demo {
protected readonly users = [
{ id: 1, name: 'Elif Kaya', role: 'Engineering' },
{ id: 2, name: 'Mert Demir', role: 'Design' },
{ id: 3, name: 'Deniz Ünal', role: 'Engineering' },
];
protected readonly assigneeId = signal<unknown>(null);
protected onSearch(text: string): void {
console.log('searching for', text);
}
}Grouping & custom values
groupBy (field name or function) groups flat data under headers on the fly — no pre-shaping. acceptCustomValue lets typed text that matches nothing become the value: customItemCreating maps it to an item (sync, async, or null to reject).
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSelectBox } from '@oge-ui/inputs';
import type { OgeSelectBoxCustomItemEvent } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeSelectBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- flat data, grouped on the fly -->
<oge-select-box
label="Team member"
[items]="users"
displayExpr="name"
valueExpr="id"
groupBy="role"
[(value)]="memberId"
/>
<!-- typed text becomes a new item -->
<oge-select-box
label="Tag"
[items]="tags()"
[searchEnabled]="true"
[acceptCustomValue]="true"
(customItemCreating)="createTag($event)"
[(value)]="tag"
/>
`,
})
export class Demo {
protected readonly users = [
{ id: 1, name: 'Elif Kaya', role: 'Engineering' },
{ id: 2, name: 'Mert Demir', role: 'Design' },
];
protected readonly memberId = signal<unknown>(null);
protected readonly tags = signal(['angular', 'signals']);
protected readonly tag = signal<unknown>(null);
protected createTag(event: OgeSelectBoxCustomItemEvent<string>): void {
event.customItem = event.text; // or a promise, or null to reject
this.tags.update((current) => [...current, event.text]);
}
}Lazy data
Pass a function as [items] — it runs once on first open; the popup shows a localized loading row while pending and an error row on rejection. selectedItem resolves as soon as the data lands.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSelectBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeSelectBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-select-box
label="Warehouse"
[items]="loadWarehouses"
[(value)]="warehouse"
/>
`,
})
export class Demo {
protected readonly warehouse = signal<unknown>(null);
// invoked once, on first open — loading/error rows render while pending
protected readonly loadWarehouses = () =>
new Promise<string[]>((resolve) =>
setTimeout(() => resolve(['Hamburg', 'İzmir', 'Rotterdam']), 900),
);
}Tag Box — multi-select
<oge-tag-box> is the multi-select sibling: the value is an array of valueExpr results, picks render as removable chips, the popup stays open while selecting (checkbox listbox, aria-multiselectable) and Backspace removes the last chip. imageExpr puts avatars on chips and options; maxDisplayedTags collapses overflow into a +N chip.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTagBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeTagBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-tag-box
label="Skills"
[items]="skills"
[searchEnabled]="true"
[showClearButton]="true"
[(value)]="selectedSkills"
(selectionChanged)="onDelta($event.addedItems, $event.removedItems)"
/>
<oge-tag-box
label="Team"
[items]="users"
displayExpr="name"
valueExpr="id"
imageExpr="avatar"
[maxDisplayedTags]="3"
[(value)]="teamIds"
/>
`,
})
export class Demo {
protected readonly skills = ['Angular', 'TypeScript', 'CSS', 'Testing'];
protected readonly selectedSkills = signal<unknown[]>(['Angular']);
protected readonly users = [
{ id: 1, name: 'Elif Kaya', avatar: '/avatars/1.png' },
{ id: 2, name: 'Mert Demir', avatar: '/avatars/2.png' },
];
protected readonly teamIds = signal<unknown[]>([]);
protected onDelta(added: readonly unknown[], removed: readonly unknown[]): void {
console.log({ added, removed });
}
}Item states & templates
disabledExpr marks rows non-selectable (skipped by keyboard navigation too). The selected value stays resolvable even while the visible list is filtered.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSelectBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeSelectBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-select-box
label="Plan"
[items]="plans"
displayExpr="name"
valueExpr="id"
disabledExpr="soldOut"
[(value)]="planId"
/>
`,
})
export class Demo {
protected readonly plans = [
{ id: 'free', name: 'Free', soldOut: false },
{ id: 'pro', name: 'Pro', soldOut: false },
{ id: 'enterprise', name: 'Enterprise', soldOut: true },
];
protected readonly planId = signal<unknown>('free');
}Field chrome
Everything from the shared chrome applies: label modes, sizes, styling modes, clear button, hints, validation subscript and the sm + subscriptSizing=none compact grid-editor shape.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSelectBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeSelectBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-select-box
label="Country"
labelMode="floating"
[items]="countries"
[showClearButton]="true"
hint="Shipping destination"
[(value)]="country"
/>
<oge-select-box
label="Country"
size="sm"
stylingMode="filled"
subscriptSizing="none"
[items]="countries"
[(value)]="country"
/>
`,
})
export class Demo {
protected readonly countries = ['Germany', 'Netherlands', 'Türkiye'];
protected readonly country = signal<unknown>(null);
}Mobile / adaptive
With adaptiveMode="auto" a viewport narrower than adaptiveBreakpoint (600px) turns the drop-down into a modal bottom sheet — titled with the label, a close button, a search field at the top, 44px rows, safe-area insets, focus trap and swipe-down dismiss — and the date picker into a full-screen dialog. Narrow the window (or open the page on a phone) to try it; the default 'none' keeps existing apps anchored.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeSelectBox, OgeTagBox, OgeDateBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeSelectBox, OgeTagBox, OgeDateBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- below 600px wide: bottom sheets / a full-screen calendar -->
<oge-select-box
label="City"
adaptiveMode="auto"
[items]="cities"
[searchEnabled]="true"
[(value)]="city"
/>
<oge-tag-box
label="Skills"
adaptiveMode="auto"
[items]="skills"
[(value)]="skillIds"
/>
<oge-date-box label="Due date" adaptiveMode="auto" [(value)]="due" />
`,
})
export class Demo {
protected readonly cities = ['Ankara', 'Berlin', 'Lisbon', 'Oslo', 'Tokyo'];
protected readonly skills = ['Angular', 'Signals', 'Nx', 'Vitest', 'SCSS'];
protected readonly city = signal<unknown>(null);
protected readonly skillIds = signal<readonly unknown[]>([]);
protected readonly due = signal<Date | null>(null);
// or for the whole app:
// providers: [provideOgeInputsConfig({ adaptiveMode: 'auto' })]
}Keyboard & accessibility
The editor implements the WAI-ARIA combobox pattern with aria-activedescendant — DOM focus never leaves the input; the active option is referenced by id and scrolled into view.
- ↓/↑ open the popup and move the active option (no wrap); Alt+↑ commits and closes.
- Enter and Space (select-only) commit; Esc closes without committing — pressed again while searching it clears the search text.
- Home/End jump to the first/last option in select-only mode (they move the caret while searching); PgUp/PgDn jump ten options.
- Printable characters type-ahead in select-only mode — a repeated character cycles through its matches.