List Box
A standing, always-open list of options as a form editor — the WAI-ARIA APG listbox (Kendo ListBox, PrimeNG Listbox): one tab stop with aria-activedescendant, arrows / Home / End / PageUp / PageDown and type-ahead, single mode where the selection follows focus and multiple mode with toggles, Shift ranges and Ctrl+A. groupBy renders labelled groups and searchEnabled a filter field.
The list machine is the dropdown editors' select-list core, generalized to an always-open list in @oge-ui/behavior — the very code the React list box runs. Works standalone via [(value)], with Signal Forms via [formField] and with reactive/template forms via formControl/ngModel.
Getting started
Arrows, Home/End and PageUp/PageDown move the active option and select it; typing jumps by prefix (accent-insensitive, so i finds İstanbul). Disabled options are skipped.
Value: 3
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeListBox } from '@oge-ui/inputs';
interface City {
id: number;
name: string;
country: string;
closed?: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeListBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- One Tab stop: the role="listbox" carries aria-activedescendant.
Arrows, Home/End, PageUp/PageDown move — and in single mode the
selection follows. Typing jumps by prefix (accent-insensitive). -->
<oge-list-box
label="City"
[items]="cities"
displayExpr="name"
valueExpr="id"
disabledExpr="closed"
[height]="220"
[(value)]="city"
/>
`,
})
export class Demo {
protected readonly cities: City[] = [
{ id: 1, name: 'Amsterdam', country: 'Netherlands' },
{ id: 2, name: 'Ankara', country: 'Türkiye' },
{ id: 3, name: 'Berlin', country: 'Germany' },
{ id: 4, name: 'Bonn', country: 'Germany', closed: true },
{ id: 5, name: 'Hamburg', country: 'Germany' },
{ id: 6, name: 'İstanbul', country: 'Türkiye' },
{ id: 7, name: 'İzmir', country: 'Türkiye' },
{ id: 8, name: 'Rotterdam', country: 'Netherlands' },
];
protected readonly city = signal<unknown>(3);
}Multiple selection
selectionMode="multiple" sets aria-multiselectable: Space, Enter and clicks toggle, Shift+arrows and Shift+click extend from the anchor, Ctrl+Shift+Home/End select to an edge, Ctrl+A selects all (again: none). The value is an items-ordered array.
Value: [2,6]
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeListBox } from '@oge-ui/inputs';
interface City {
id: number;
name: string;
country: string;
closed?: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeListBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- aria-multiselectable: Space / Enter and clicks toggle, Shift+arrows
and Shift+click extend from the anchor, Ctrl+Shift+Home/End select to
an edge, Ctrl+A selects all. The value is an items-ordered array. -->
<oge-list-box
label="Cities to visit"
[items]="cities"
displayExpr="name"
valueExpr="id"
disabledExpr="closed"
selectionMode="multiple"
[showCheckBoxes]="true"
[height]="220"
[(value)]="picked"
/>
`,
})
export class Demo {
protected readonly cities: City[] = [
{ id: 1, name: 'Amsterdam', country: 'Netherlands' },
{ id: 2, name: 'Ankara', country: 'Türkiye' },
{ id: 3, name: 'Berlin', country: 'Germany' },
{ id: 4, name: 'Bonn', country: 'Germany', closed: true },
{ id: 5, name: 'Hamburg', country: 'Germany' },
{ id: 6, name: 'İstanbul', country: 'Türkiye' },
{ id: 7, name: 'İzmir', country: 'Türkiye' },
{ id: 8, name: 'Rotterdam', country: 'Netherlands' },
];
protected readonly picked = signal<unknown>([2, 6]);
}Groups and search
groupBy renders each group as a role="group" labelled by its header; searchEnabled adds a filter field above the list (ArrowDown moves into the list).
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeListBox } from '@oge-ui/inputs';
interface City {
id: number;
name: string;
country: string;
closed?: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeListBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- groupBy renders each group as a role="group" labelled by its
header; searchEnabled adds a filter field (ArrowDown enters the list). -->
<oge-list-box
label="Office"
[items]="cities"
displayExpr="name"
valueExpr="id"
groupBy="country"
[searchEnabled]="true"
[height]="260"
[(value)]="office"
/>
`,
})
export class Demo {
protected readonly cities: City[] = [
{ id: 1, name: 'Amsterdam', country: 'Netherlands' },
{ id: 2, name: 'Ankara', country: 'Türkiye' },
{ id: 3, name: 'Berlin', country: 'Germany' },
{ id: 4, name: 'Bonn', country: 'Germany', closed: true },
{ id: 5, name: 'Hamburg', country: 'Germany' },
{ id: 6, name: 'İstanbul', country: 'Türkiye' },
{ id: 7, name: 'İzmir', country: 'Türkiye' },
{ id: 8, name: 'Rotterdam', country: 'Netherlands' },
];
protected readonly office = signal<unknown>(null);
}Custom templates
The option keeps its role, selection state and check glyph — only the content is yours. The group template receives the label and the option count.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeListBox, OgeListBoxItemTemplate, OgeListBoxGroupTemplate } from '@oge-ui/inputs';
interface City {
id: number;
name: string;
country: string;
closed?: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeListBox, OgeListBoxItemTemplate, OgeListBoxGroupTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The option keeps its role, state and check glyph; only its content
is yours. The group template gets the label and the option count. -->
<oge-list-box
label="Cities"
[items]="cities"
displayExpr="name"
valueExpr="id"
groupBy="country"
selectionMode="multiple"
[height]="260"
[(value)]="templated"
>
<ng-template ogeListBoxItemTemplate let-city let-selected="selected">
<span class="demo-city">
<span class="demo-city-badge">{{ city.name.charAt(0) }}</span>
{{ city.name }}
@if (selected) {
<small>selected</small>
}
</span>
</ng-template>
<ng-template ogeListBoxGroupTemplate let-label let-count="count">
{{ label }} · {{ count }}
</ng-template>
</oge-list-box>
`,
})
export class Demo {
protected readonly cities: City[] = [
{ id: 1, name: 'Amsterdam', country: 'Netherlands' },
{ id: 2, name: 'Ankara', country: 'Türkiye' },
{ id: 3, name: 'Berlin', country: 'Germany' },
{ id: 4, name: 'Bonn', country: 'Germany', closed: true },
{ id: 5, name: 'Hamburg', country: 'Germany' },
{ id: 6, name: 'İstanbul', country: 'Türkiye' },
{ id: 7, name: 'İzmir', country: 'Türkiye' },
{ id: 8, name: 'Rotterdam', country: 'Netherlands' },
];
protected readonly templated = signal<unknown>([1]);
}Inside a form
A FormValueControl and a ControlValueAccessor: formControl, ngModel and [formField] all bind it. In multiple mode required means at least one option.
Valid: false
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeListBox } from '@oge-ui/inputs';
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
interface City {
id: number;
name: string;
country: string;
closed?: boolean;
}
@Component({
selector: 'demo-root',
imports: [OgeListBox, ReactiveFormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- FormValueControl + ControlValueAccessor: formControl, ngModel and
[formField] bind it; required means "at least one" in multiple mode. -->
<oge-list-box
label="Destinations"
hint="Pick at least one"
[items]="cities"
displayExpr="name"
valueExpr="id"
selectionMode="multiple"
[height]="200"
[formControl]="destinations"
/>
`,
})
export class Demo {
protected readonly cities: City[] = [
{ id: 1, name: 'Amsterdam', country: 'Netherlands' },
{ id: 2, name: 'Ankara', country: 'Türkiye' },
{ id: 3, name: 'Berlin', country: 'Germany' },
{ id: 4, name: 'Bonn', country: 'Germany', closed: true },
{ id: 5, name: 'Hamburg', country: 'Germany' },
{ id: 6, name: 'İstanbul', country: 'Türkiye' },
{ id: 7, name: 'İzmir', country: 'Türkiye' },
{ id: 8, name: 'Rotterdam', country: 'Netherlands' },
];
protected readonly destinations = new FormControl<number[]>([], Validators.required);
}