Skip to content

khalilElmouedene/ngx-mat-searchable-select

Repository files navigation

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 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 MockSearchableSelectDataSource for 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

Contributions

Contributions are welcome, please open an issue and preferably file a pull request.

Installation

npm install ngx-mat-searchable-select

Quick Start

Option A — Static items (no backend needed)

The 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);
  }
}

Option B — Mock data source with pagination

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);
  }
}

Option C — Server-driven with real pagination

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,
};

Option D — 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',
};

Option E — Multiple selection

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,
};

API Reference

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
  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 fontIcon for quick setup with Material Icons (just add the font link). Use svgIcon when you have custom SVG icons registered via MatIconRegistry. 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

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

Running the Demo

The repo includes a full demo application under projects/demo/.

# Install dependencies
npm install

# Serve the demo locally
ng serve demo

Then open http://localhost:4200. The demo showcases:

  1. Static Items — pass an array, no backend needed
  2. Mock DataSource — simulated pagination and search
  3. Edit Mode — pre-selected value shown at the top
  4. Multiple Select — pick several items at once
  5. No Search — dropdown without the search box

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 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

License

MIT

About

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.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors