ngx-mat-searchable-select
v2.0.4
Published
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.
Readme
ngx-mat-searchable-select
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.
Features
- Infinite scroll pagination for large server-side datasets
- Debounced search with a sticky search bar, search icon, 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
Requirements
- Angular
^21.1.0 - Angular Material
^21.1.0 - RxJS
~7.8.0
Try it
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
Contributions are welcome, please open an issue and preferably file a pull request.
Installation
npm install ngx-mat-searchable-selectQuick Start
Option A — Static items (no backend 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',
},
mode: 'create',
searchable: true,
};
onSelect(event: any) {
console.log('Selected:', event.value);
}
}Option B — Server-driven with pagination
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 in your component:
config: SearchableSelectConfig = {
dataSource: inject(CityService),
option: {
label: 'City',
formControlName: 'city',
displayName: 'name',
isRequired: true,
fontIcon: 'location_city',
},
mode: 'create',
searchable: true,
};Option C — Edit mode (pre-selected value)
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',
};API
Inputs
| 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 |
Outputs
| Output | Type | Description |
|---|---|---|
| selectionChange | MatSelectChange | Emits when the user picks an option |
| valueChange | unknown | Emits the raw value on every change |
SearchableSelectConfig
interface SearchableSelectConfig {
dataSource?: SearchableSelectDataSource; // required when staticItems is not provided
option: SearchableSelectOption;
mode: 'create' | 'edit';
filter?: { id?: number }; // extra server-side filter (e.g. parent entity id)
searchable?: boolean; // show search box (default: true)
multiple?: boolean; // allow multi-select (default: false)
}SearchableSelectOption
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 include the font link in yourindex.html). UsesvgIconwhen you have custom SVG icons registered viaMatIconRegistry. Both are optional — if neither is set, no prefix icon is shown.
SearchableSelectDataSource
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;
}MockSearchableSelectDataSource
For use in demos, StackBlitz, and unit tests:
import { MockSearchableSelectDataSource } from 'ngx-mat-searchable-select';
const dataSource = new MockSearchableSelectDataSource([
{ id: 1, name: 'Paris' },
{ id: 2, name: 'London' },
]);
config: SearchableSelectConfig = {
dataSource,
option: { ... },
mode: 'create',
};MockSearchableSelectDataSource supports:
- Pagination via
skip/take - Full-text search across all string-coercible fields
Migration from v1
| v1 | v2 |
|---|---|
| mat-list-shared (npm) | ngx-mat-searchable-select |
| <lib-mat-list-shared> | <ngx-mat-searchable-select> |
| MatListSharedComponent | NgxMatSearchableSelectComponent |
| MatListSharedModule | removed — import 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 |
