A reusable Angular Material select component with infinite scroll, debounced search, "no items found" feedback, and static/mock data support — built with Angular 21 standalone components and signals.
- Infinite scroll pagination for large server-side datasets
- Debounced search with a sticky search bar and clear button
- "No items found" message when search returns no results
- Single and multiple selection
- Edit mode — keeps the pre-selected item visible even when not in the loaded page
- Static items input — no server required for demos and tests
- Built-in
MockSearchableSelectDataSourcefor development and unit tests - Font icon and SVG icon support (no icon registration required for font icons)
- Fully standalone, signal-based, zoneless-ready
- Angular
^21.1.0 - Angular Material
^21.1.0 - RxJS
~7.8.0
See it in action at
-
[https://stackblitz.com/~/github.com/khalilElmouedene/ngx-mat-searchable-select]
see example code, builds in browser, latest version, latest material version
-
[https://github.com/khalilElmouedene/ngx-mat-searchable-select]
pre-built, latest version, works on mobile
Contributions are welcome, please open an issue and preferably file a pull request.
npm install ngx-mat-searchable-selectThe simplest way to use the component. Pass an array directly via [staticItems] — no dataSource needed.
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import {
NgxMatSearchableSelectComponent,
SearchableSelectConfig,
} from 'ngx-mat-searchable-select';
@Component({
selector: 'app-example',
standalone: true,
imports: [ReactiveFormsModule, NgxMatSearchableSelectComponent],
template: `
<ngx-mat-searchable-select
[parentForm]="form"
[config]="config"
[staticItems]="cities"
(selectionChange)="onSelect($event)"
/>
`,
})
export class ExampleComponent {
form = inject(FormBuilder).group({
city: [null, Validators.required],
});
cities = [
{ id: 1, name: 'Paris' },
{ id: 2, name: 'London' },
{ id: 3, name: 'Berlin' },
];
config: SearchableSelectConfig = {
option: {
label: 'City',
formControlName: 'city',
displayName: 'name',
isRequired: true,
fontIcon: 'location_city', // Material font icon (no registration needed)
},
mode: 'create',
searchable: true,
};
onSelect(event: any) {
console.log('Selected:', event.value);
}
}Use MockSearchableSelectDataSource for demos and tests. It simulates server-side pagination and full-text search over a static array.
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import {
NgxMatSearchableSelectComponent,
SearchableSelectConfig,
MockSearchableSelectDataSource,
} from 'ngx-mat-searchable-select';
@Component({
selector: 'app-example',
standalone: true,
imports: [ReactiveFormsModule, NgxMatSearchableSelectComponent],
template: `
<ngx-mat-searchable-select
[parentForm]="form"
[config]="config"
(selectionChange)="onSelect($event)"
/>
`,
})
export class ExampleComponent {
form = inject(FormBuilder).group({ language: [null] });
config: SearchableSelectConfig = {
dataSource: new MockSearchableSelectDataSource([
{ id: 1, name: 'TypeScript' },
{ id: 2, name: 'JavaScript' },
{ id: 3, name: 'Python' },
{ id: 4, name: 'Rust' },
{ id: 5, name: 'Go' },
]),
option: {
label: 'Language',
formControlName: 'language',
displayName: 'name',
isRequired: false,
fontIcon: 'code',
},
mode: 'create',
searchable: true,
};
onSelect(event: any) {
console.log('Selected:', event.value);
}
}Implement SearchableSelectDataSource in your service to connect to a real API.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import {
SearchableSelectDataSource,
PagedRequest,
PagedResponse,
} from 'ngx-mat-searchable-select';
@Injectable({ providedIn: 'root' })
export class CityService implements SearchableSelectDataSource {
private http = inject(HttpClient);
getAll(request: PagedRequest): Observable<PagedResponse<Record<string, unknown>>> {
return this.http.get<PagedResponse<Record<string, unknown>>>('/api/cities', {
params: {
skip: request.skip,
take: request.take,
search: request.searchString,
},
});
}
}Then use it in your component:
config: SearchableSelectConfig = {
dataSource: inject(CityService),
option: {
label: 'City',
formControlName: 'city',
displayName: 'name',
isRequired: true,
fontIcon: 'location_city',
},
mode: 'create',
searchable: true,
};When mode: 'edit' and the pre-selected item may not be on the first loaded page, the component automatically shows it as an extra option at the top.
config: SearchableSelectConfig = {
dataSource: inject(CityService),
option: {
label: 'City',
formControlName: 'city',
displayName: 'name',
isRequired: true,
fontIcon: 'location_city',
currentId: 42, // the pre-selected item's id
currentLabel: 'Madrid', // its display label
},
mode: 'edit',
};config: SearchableSelectConfig = {
dataSource: new MockSearchableSelectDataSource(cities),
option: {
label: 'Favorite cities',
formControlName: 'cities',
displayName: 'name',
isRequired: false,
fontIcon: 'location_city',
},
mode: 'create',
searchable: true,
multiple: true,
};| Input | Type | Required | Description |
|---|---|---|---|
parentForm |
FormGroup |
Yes | The parent reactive form that contains the control |
config |
SearchableSelectConfig |
Yes | Component configuration object |
staticItems |
Record<string, unknown>[] |
No | Static item array — bypasses dataSource entirely |
| Output | Type | Description |
|---|---|---|
selectionChange |
MatSelectChange |
Emits when the user picks an option |
valueChange |
unknown |
Emits the raw value on every change |
interface SearchableSelectConfig {
dataSource?: SearchableSelectDataSource; // required when staticItems is not provided
option: SearchableSelectOption;
mode: 'create' | 'edit';
filter?: { id?: number }; // extra server-side filter
searchable?: boolean; // show search box (default: true)
multiple?: boolean; // allow multi-select (default: false)
}interface SearchableSelectOption {
isRequired: boolean; // mark the field as required
displayName: string; // property key used to display each item (e.g. 'name')
formControlName: string; // form control name in the parent FormGroup
label: string; // dropdown label text
svgIcon?: string; // Material SVG icon name (requires MatIconRegistry)
fontIcon?: string; // Material font icon name (no registration needed)
currentId?: number; // pre-selected item id (edit mode)
currentLabel?: string; // pre-selected item label (edit mode)
}Icon usage: Use
fontIconfor quick setup with Material Icons (just add the font link). UsesvgIconwhen you have custom SVG icons registered viaMatIconRegistry. Both are optional — if neither is set, no prefix icon is shown.
interface SearchableSelectDataSource {
getAll(request: PagedRequest): Observable<PagedResponse<Record<string, unknown>>>;
}
interface PagedRequest {
skip: number;
take: number;
searchString: string;
sort?: string;
id?: number;
}
interface PagedResponse<T> {
data: T[];
totalCount: number;
}A ready-made in-memory data source for demos, StackBlitz, and unit tests:
import { MockSearchableSelectDataSource } from 'ngx-mat-searchable-select';
const dataSource = new MockSearchableSelectDataSource([
{ id: 1, name: 'Paris' },
{ id: 2, name: 'London' },
]);It supports:
- Pagination via
skip/take - Full-text search across all string-coercible fields
- Returns
Observable<PagedResponse>just like a real server
The repo includes a full demo application under projects/demo/.
# Install dependencies
npm install
# Serve the demo locally
ng serve demoThen open http://localhost:4200. The demo showcases:
- Static Items — pass an array, no backend needed
- Mock DataSource — simulated pagination and search
- Edit Mode — pre-selected value shown at the top
- Multiple Select — pick several items at once
- No Search — dropdown without the search box
| v1 | v2 |
|---|---|
mat-list-shared (npm) |
ngx-mat-searchable-select |
<lib-mat-list-shared> |
<ngx-mat-searchable-select> |
MatListSharedComponent |
NgxMatSearchableSelectComponent |
MatListSharedModule |
removed — import the component directly |
CustomMatList class |
SearchableSelectConfig interface |
IMatListService |
SearchableSelectDataSource |
IMatListOption |
SearchableSelectOption |
IPagedMatListRequestDto |
PagedRequest |
[reservedForm] |
[parentForm] |
[matList] |
[config] |
option.text |
option.label |
option.svgIcon (required) |
option.svgIcon or option.fontIcon (both optional) |
option.optionId |
option.currentId |
option.optionLibelle |
option.currentLabel |
searchAble |
searchable |
isMultiSelect |
multiple |
typeAction |
mode |
filtre |
filter |
service |
dataSource |
MIT