ngx-mat-searchable-select 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +19 -127
  2. package/package.json +12 -44
  3. package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.component.html +75 -75
  4. package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.mock.ts +43 -43
  5. package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-select-infinite-scroll.directive.ts +52 -52
  6. package/.editorconfig +0 -16
  7. package/.vscode/extensions.json +0 -4
  8. package/.vscode/launch.json +0 -20
  9. package/.vscode/tasks.json +0 -42
  10. package/LICENSE +0 -21
  11. package/angular.json +0 -151
  12. package/projects/demo/public/favicon.ico +0 -0
  13. package/projects/demo/src/app/app.config.ts +0 -9
  14. package/projects/demo/src/app/app.html +0 -118
  15. package/projects/demo/src/app/app.scss +0 -64
  16. package/projects/demo/src/app/app.ts +0 -167
  17. package/projects/demo/src/index.html +0 -16
  18. package/projects/demo/src/main.ts +0 -6
  19. package/projects/demo/src/styles.scss +0 -31
  20. package/projects/demo/tsconfig.app.json +0 -17
  21. package/projects/demo/tsconfig.spec.json +0 -15
  22. package/projects/ngx-mat-searchable-select/README.md +0 -262
  23. package/projects/ngx-mat-searchable-select/package.json +0 -12
  24. package/tsconfig.json +0 -39
  25. /package/{projects/ngx-mat-searchable-select/karma.conf.js → karma.conf.js} +0 -0
  26. /package/{projects/ngx-mat-searchable-select/ng-package.json → ng-package.json} +0 -0
  27. /package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.component.spec.ts +0 -0
  28. /package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.component.ts +0 -0
  29. /package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.model.ts +0 -0
  30. /package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.service.spec.ts +0 -0
  31. /package/{projects/ngx-mat-searchable-select/src → src}/public-api.ts +0 -0
  32. /package/{projects/ngx-mat-searchable-select/src → src}/test.ts +0 -0
  33. /package/{projects/ngx-mat-searchable-select/tsconfig.lib.json → tsconfig.lib.json} +0 -0
  34. /package/{projects/ngx-mat-searchable-select/tsconfig.lib.prod.json → tsconfig.lib.prod.json} +0 -0
  35. /package/{projects/ngx-mat-searchable-select/tsconfig.spec.json → tsconfig.spec.json} +0 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@ A reusable Angular Material select component with **infinite scroll**, **debounc
5
5
  ## Features
6
6
 
7
7
  - Infinite scroll pagination for large server-side datasets
8
- - Debounced search with a sticky search bar and clear button
8
+ - Debounced search with a sticky search bar, search icon, and clear button
9
9
  - "No items found" message when search returns no results
10
10
  - Single and multiple selection
11
11
  - Edit mode — keeps the pre-selected item visible even when not in the loaded page
@@ -20,21 +20,6 @@ A reusable Angular Material select component with **infinite scroll**, **debounc
20
20
  - Angular Material `^21.1.0`
21
21
  - RxJS `~7.8.0`
22
22
 
23
- ## Try it
24
- See it in action at
25
-
26
- * [https://stackblitz.com/~/github.com/khalilElmouedene/ngx-mat-searchable-select]
27
-
28
- see example code, builds in browser, latest version, latest material version
29
- * [https://github.com/khalilElmouedene/ngx-mat-searchable-select]
30
-
31
- pre-built, latest version, works on mobile
32
-
33
-
34
- ## Contributions
35
- Contributions are welcome, please open an issue and preferably file a pull request.
36
-
37
-
38
23
  ## Installation
39
24
 
40
25
  ```bash
@@ -45,8 +30,6 @@ npm install ngx-mat-searchable-select
45
30
 
46
31
  ### Option A — Static items (no backend needed)
47
32
 
48
- The simplest way to use the component. Pass an array directly via `[staticItems]` — no `dataSource` needed.
49
-
50
33
  ```typescript
51
34
  import { Component, inject } from '@angular/core';
52
35
  import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@@ -85,60 +68,7 @@ export class ExampleComponent {
85
68
  formControlName: 'city',
86
69
  displayName: 'name',
87
70
  isRequired: true,
88
- fontIcon: 'location_city', // Material font icon (no registration needed)
89
- },
90
- mode: 'create',
91
- searchable: true,
92
- };
93
-
94
- onSelect(event: any) {
95
- console.log('Selected:', event.value);
96
- }
97
- }
98
- ```
99
-
100
- ### Option B — Mock data source with pagination
101
-
102
- Use `MockSearchableSelectDataSource` for demos and tests. It simulates server-side pagination and full-text search over a static array.
103
-
104
- ```typescript
105
- import { Component, inject } from '@angular/core';
106
- import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
107
- import {
108
- NgxMatSearchableSelectComponent,
109
- SearchableSelectConfig,
110
- MockSearchableSelectDataSource,
111
- } from 'ngx-mat-searchable-select';
112
-
113
- @Component({
114
- selector: 'app-example',
115
- standalone: true,
116
- imports: [ReactiveFormsModule, NgxMatSearchableSelectComponent],
117
- template: `
118
- <ngx-mat-searchable-select
119
- [parentForm]="form"
120
- [config]="config"
121
- (selectionChange)="onSelect($event)"
122
- />
123
- `,
124
- })
125
- export class ExampleComponent {
126
- form = inject(FormBuilder).group({ language: [null] });
127
-
128
- config: SearchableSelectConfig = {
129
- dataSource: new MockSearchableSelectDataSource([
130
- { id: 1, name: 'TypeScript' },
131
- { id: 2, name: 'JavaScript' },
132
- { id: 3, name: 'Python' },
133
- { id: 4, name: 'Rust' },
134
- { id: 5, name: 'Go' },
135
- ]),
136
- option: {
137
- label: 'Language',
138
- formControlName: 'language',
139
- displayName: 'name',
140
- isRequired: false,
141
- fontIcon: 'code',
71
+ fontIcon: 'location_city',
142
72
  },
143
73
  mode: 'create',
144
74
  searchable: true,
@@ -150,9 +80,7 @@ export class ExampleComponent {
150
80
  }
151
81
  ```
152
82
 
153
- ### Option C — Server-driven with real pagination
154
-
155
- Implement `SearchableSelectDataSource` in your service to connect to a real API.
83
+ ### Option B — Server-driven with pagination
156
84
 
157
85
  ```typescript
158
86
  import { Injectable, inject } from '@angular/core';
@@ -180,7 +108,7 @@ export class CityService implements SearchableSelectDataSource {
180
108
  }
181
109
  ```
182
110
 
183
- Then use it in your component:
111
+ Then in your component:
184
112
 
185
113
  ```typescript
186
114
  config: SearchableSelectConfig = {
@@ -197,9 +125,10 @@ config: SearchableSelectConfig = {
197
125
  };
198
126
  ```
199
127
 
200
- ### Option D — Edit mode (pre-selected value)
128
+ ### Option C — Edit mode (pre-selected value)
201
129
 
202
- 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.
130
+ When `mode: 'edit'` and the pre-selected item may not be on the first loaded page,
131
+ the component automatically shows it as an extra option at the top.
203
132
 
204
133
  ```typescript
205
134
  config: SearchableSelectConfig = {
@@ -217,25 +146,7 @@ config: SearchableSelectConfig = {
217
146
  };
218
147
  ```
219
148
 
220
- ### Option E — Multiple selection
221
-
222
- ```typescript
223
- config: SearchableSelectConfig = {
224
- dataSource: new MockSearchableSelectDataSource(cities),
225
- option: {
226
- label: 'Favorite cities',
227
- formControlName: 'cities',
228
- displayName: 'name',
229
- isRequired: false,
230
- fontIcon: 'location_city',
231
- },
232
- mode: 'create',
233
- searchable: true,
234
- multiple: true,
235
- };
236
- ```
237
-
238
- ## API Reference
149
+ ## API
239
150
 
240
151
  ### Inputs
241
152
 
@@ -259,7 +170,7 @@ interface SearchableSelectConfig {
259
170
  dataSource?: SearchableSelectDataSource; // required when staticItems is not provided
260
171
  option: SearchableSelectOption;
261
172
  mode: 'create' | 'edit';
262
- filter?: { id?: number }; // extra server-side filter
173
+ filter?: { id?: number }; // extra server-side filter (e.g. parent entity id)
263
174
  searchable?: boolean; // show search box (default: true)
264
175
  multiple?: boolean; // allow multi-select (default: false)
265
176
  }
@@ -280,7 +191,7 @@ interface SearchableSelectOption {
280
191
  }
281
192
  ```
282
193
 
283
- > **Icon usage:** Use `fontIcon` for quick setup with [Material Icons](https://fonts.google.com/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.
194
+ > **Icon usage:** Use `fontIcon` for quick setup with [Material Icons](https://fonts.google.com/icons) (just include the font link in your `index.html`). Use `svgIcon` when you have custom SVG icons registered via `MatIconRegistry`. Both are optional — if neither is set, no prefix icon is shown.
284
195
 
285
196
  ### SearchableSelectDataSource
286
197
 
@@ -305,7 +216,7 @@ interface PagedResponse<T> {
305
216
 
306
217
  ## MockSearchableSelectDataSource
307
218
 
308
- A ready-made in-memory data source for demos, StackBlitz, and unit tests:
219
+ For use in demos, StackBlitz, and unit tests:
309
220
 
310
221
  ```typescript
311
222
  import { MockSearchableSelectDataSource } from 'ngx-mat-searchable-select';
@@ -314,32 +225,17 @@ const dataSource = new MockSearchableSelectDataSource([
314
225
  { id: 1, name: 'Paris' },
315
226
  { id: 2, name: 'London' },
316
227
  ]);
228
+
229
+ config: SearchableSelectConfig = {
230
+ dataSource,
231
+ option: { ... },
232
+ mode: 'create',
233
+ };
317
234
  ```
318
235
 
319
- It supports:
236
+ `MockSearchableSelectDataSource` supports:
320
237
  - Pagination via `skip` / `take`
321
238
  - Full-text search across all string-coercible fields
322
- - Returns `Observable<PagedResponse>` just like a real server
323
-
324
- ## Running the Demo
325
-
326
- The repo includes a full demo application under `projects/demo/`.
327
-
328
- ```bash
329
- # Install dependencies
330
- npm install
331
-
332
- # Serve the demo locally
333
- ng serve demo
334
- ```
335
-
336
- Then open [http://localhost:4200](http://localhost:4200). The demo showcases:
337
-
338
- 1. **Static Items** — pass an array, no backend needed
339
- 2. **Mock DataSource** — simulated pagination and search
340
- 3. **Edit Mode** — pre-selected value shown at the top
341
- 4. **Multiple Select** — pick several items at once
342
- 5. **No Search** — dropdown without the search box
343
239
 
344
240
  ## Migration from v1
345
241
 
@@ -348,7 +244,7 @@ Then open [http://localhost:4200](http://localhost:4200). The demo showcases:
348
244
  | `mat-list-shared` (npm) | `ngx-mat-searchable-select` |
349
245
  | `<lib-mat-list-shared>` | `<ngx-mat-searchable-select>` |
350
246
  | `MatListSharedComponent` | `NgxMatSearchableSelectComponent` |
351
- | `MatListSharedModule` | removed — import the component directly |
247
+ | `MatListSharedModule` | removed — import component directly |
352
248
  | `CustomMatList` class | `SearchableSelectConfig` interface |
353
249
  | `IMatListService` | `SearchableSelectDataSource` |
354
250
  | `IMatListOption` | `SearchableSelectOption` |
@@ -364,7 +260,3 @@ Then open [http://localhost:4200](http://localhost:4200). The demo showcases:
364
260
  | `typeAction` | `mode` |
365
261
  | `filtre` | `filter` |
366
262
  | `service` | `dataSource` |
367
-
368
- ## License
369
-
370
- MIT
package/package.json CHANGED
@@ -1,44 +1,12 @@
1
- {
2
- "name": "ngx-mat-searchable-select",
3
- "version": "1.0.0",
4
- "scripts": {
5
- "ng": "ng",
6
- "start": "ng serve",
7
- "build": "ng build",
8
- "watch": "ng build --watch --configuration development",
9
- "test": "ng test"
10
- },
11
- "license": "MIT",
12
- "private": false,
13
- "dependencies": {
14
- "@angular/animations": "~21.1.3",
15
- "@angular/cdk": "^21.1.3",
16
- "@angular/common": "~21.1.3",
17
- "@angular/compiler": "~21.1.3",
18
- "@angular/core": "~21.1.3",
19
- "@angular/forms": "~21.1.3",
20
- "@angular/material": "^21.1.3",
21
- "@angular/platform-browser": "~21.1.3",
22
- "@angular/platform-browser-dynamic": "~21.1.3",
23
- "@angular/router": "~21.1.3",
24
- "rxjs": "~7.8.0",
25
- "tslib": "^2.3.0",
26
- "zone.js": "~0.15.1"
27
- },
28
- "devDependencies": {
29
- "@angular-devkit/build-angular": "^21.1.3",
30
- "@angular/build": "^21.1.3",
31
- "@angular/cli": "~21.1.3",
32
- "@angular/compiler-cli": "~21.1.3",
33
- "@types/jasmine": "~3.10.0",
34
- "@types/node": "^12.11.1",
35
- "jasmine-core": "~4.0.0",
36
- "karma": "~6.4.0",
37
- "karma-chrome-launcher": "~3.1.0",
38
- "karma-coverage": "~2.1.0",
39
- "karma-jasmine": "~4.0.0",
40
- "karma-jasmine-html-reporter": "~1.7.0",
41
- "ng-packagr": "^21.1.0",
42
- "typescript": "~5.9.3"
43
- }
44
- }
1
+ {
2
+ "name": "ngx-mat-searchable-select",
3
+ "version": "2.0.0",
4
+ "license": "MIT",
5
+ "peerDependencies": {
6
+ "@angular/common": "^21.1.3",
7
+ "@angular/core": "^21.1.3",
8
+ "@angular/forms": "^21.1.3",
9
+ "@angular/material": "^21.1.3",
10
+ "rxjs": "~7.8.0"
11
+ }
12
+ }
@@ -1,75 +1,75 @@
1
- <mat-form-field style="width: 100%" [formGroup]="parentForm()">
2
- <mat-label>{{ config().option.label }}</mat-label>
3
-
4
- <mat-select
5
- matSelectInfiniteScroll
6
- (infiniteScroll)="fetchNextPage()"
7
- [required]="config().option.isRequired"
8
- [complete]="isScrollComplete()"
9
- [formControlName]="config().option.formControlName"
10
- (selectionChange)="onSelectionChange($event)"
11
- (valueChange)="onValueChange($event)"
12
- (closed)="onPanelClosed()"
13
- [multiple]="config().multiple ?? false"
14
- >
15
- @if (config().searchable !== false) {
16
- <div style="padding: 8px 16px 0; position: sticky; top: 0; z-index: 1;" [formGroup]="searchForm">
17
- <mat-form-field style="width: 100%">
18
- <mat-icon matPrefix style="margin-right: 8px; color: rgba(0,0,0,.54)">search</mat-icon>
19
- <input
20
- matInput
21
- formControlName="searchTerm"
22
- (input)="onSearchInput()"
23
- placeholder="Type to search..."
24
- autocomplete="off"
25
- style="padding: 4px 0;"
26
- />
27
- @if (searchForm.get('searchTerm')?.value) {
28
- <button
29
- mat-icon-button
30
- matSuffix
31
- aria-label="Clear"
32
- (click)="resetSearch()"
33
- >
34
- <mat-icon>close</mat-icon>
35
- </button>
36
- }
37
- </mat-form-field>
38
- </div>
39
- }
40
-
41
- @if (showEditOption()) {
42
- <mat-option [value]="config().option.currentId">
43
- {{ config().option.currentLabel }}
44
- </mat-option>
45
- }
46
-
47
- @for (item of items(); track item['id']) {
48
- <mat-option [value]="item['id']">
49
- {{ getDisplayLabel(item) }}
50
- </mat-option>
51
- }
52
-
53
- @if (isLoading()) {
54
- <mat-option disabled>Loading...</mat-option>
55
- }
56
-
57
- @if (noItemsFound()) {
58
- <mat-option disabled>
59
- <span style="color: rgba(0,0,0,.54); font-style: italic;">No items found</span>
60
- </mat-option>
61
- }
62
- </mat-select>
63
-
64
- @if (config().option.svgIcon) {
65
- <mat-icon class="icon-size-5 text-primary" matPrefix [svgIcon]="config().option.svgIcon!"></mat-icon>
66
- } @else if (config().option.fontIcon) {
67
- <mat-icon class="icon-size-5 text-primary" matPrefix>{{ config().option.fontIcon }}</mat-icon>
68
- }
69
-
70
- @if (parentForm().get(config().option.formControlName)?.hasError('required')) {
71
- <mat-error>
72
- {{ config().option.label }} is required
73
- </mat-error>
74
- }
75
- </mat-form-field>
1
+ <mat-form-field style="width: 100%" [formGroup]="parentForm()">
2
+ <mat-label>{{ config().option.label }}</mat-label>
3
+
4
+ <mat-select
5
+ matSelectInfiniteScroll
6
+ (infiniteScroll)="fetchNextPage()"
7
+ [required]="config().option.isRequired"
8
+ [complete]="isScrollComplete()"
9
+ [formControlName]="config().option.formControlName"
10
+ (selectionChange)="onSelectionChange($event)"
11
+ (valueChange)="onValueChange($event)"
12
+ (closed)="onPanelClosed()"
13
+ [multiple]="config().multiple ?? false"
14
+ >
15
+ @if (config().searchable !== false) {
16
+ <div style="padding: 8px 16px 0; position: sticky; top: 0; z-index: 1;" [formGroup]="searchForm">
17
+ <mat-form-field style="width: 100%">
18
+ <mat-icon matPrefix style="margin-right: 8px; color: rgba(0,0,0,.54)">search</mat-icon>
19
+ <input
20
+ matInput
21
+ formControlName="searchTerm"
22
+ (input)="onSearchInput()"
23
+ placeholder="Type to search..."
24
+ autocomplete="off"
25
+ style="padding: 4px 0;"
26
+ />
27
+ @if (searchForm.get('searchTerm')?.value) {
28
+ <button
29
+ mat-icon-button
30
+ matSuffix
31
+ aria-label="Clear"
32
+ (click)="resetSearch()"
33
+ >
34
+ <mat-icon>close</mat-icon>
35
+ </button>
36
+ }
37
+ </mat-form-field>
38
+ </div>
39
+ }
40
+
41
+ @if (showEditOption()) {
42
+ <mat-option [value]="config().option.currentId">
43
+ {{ config().option.currentLabel }}
44
+ </mat-option>
45
+ }
46
+
47
+ @for (item of items(); track item['id']) {
48
+ <mat-option [value]="item['id']">
49
+ {{ getDisplayLabel(item) }}
50
+ </mat-option>
51
+ }
52
+
53
+ @if (isLoading()) {
54
+ <mat-option disabled>Loading...</mat-option>
55
+ }
56
+
57
+ @if (noItemsFound()) {
58
+ <mat-option disabled>
59
+ <span style="color: rgba(0,0,0,.54); font-style: italic;">No items found</span>
60
+ </mat-option>
61
+ }
62
+ </mat-select>
63
+
64
+ @if (config().option.svgIcon) {
65
+ <mat-icon class="icon-size-5 text-primary" matPrefix [svgIcon]="config().option.svgIcon!"></mat-icon>
66
+ } @else if (config().option.fontIcon) {
67
+ <mat-icon class="icon-size-5 text-primary" matPrefix>{{ config().option.fontIcon }}</mat-icon>
68
+ }
69
+
70
+ @if (parentForm().get(config().option.formControlName)?.hasError('required')) {
71
+ <mat-error>
72
+ {{ config().option.label }} is required
73
+ </mat-error>
74
+ }
75
+ </mat-form-field>
@@ -1,43 +1,43 @@
1
- import { Observable, of } from 'rxjs';
2
- import { SearchableSelectDataSource, PagedRequest, PagedResponse } from './mat-list-shared.model';
3
-
4
- /**
5
- * A ready-made data source backed by a static in-memory array.
6
- *
7
- * Use this when you want to demo or test the component without a real API:
8
- *
9
- * ```ts
10
- * const dataSource = new MockSearchableSelectDataSource([
11
- * { id: 1, name: 'Paris' },
12
- * { id: 2, name: 'London' },
13
- * ]);
14
- *
15
- * this.config = {
16
- * dataSource,
17
- * option: { ... },
18
- * mode: 'create',
19
- * };
20
- * ```
21
- *
22
- * Search is applied across all string-coercible fields of each item.
23
- * Pagination uses the `skip` / `take` values from the request.
24
- */
25
- export class MockSearchableSelectDataSource implements SearchableSelectDataSource {
26
- constructor(private readonly items: Record<string, unknown>[]) {}
27
-
28
- getAll(request: PagedRequest): Observable<PagedResponse<Record<string, unknown>>> {
29
- const term = request.searchString?.toLowerCase() ?? '';
30
-
31
- const filtered = term
32
- ? this.items.filter(item =>
33
- Object.values(item).some(value =>
34
- String(value ?? '').toLowerCase().includes(term)
35
- )
36
- )
37
- : this.items;
38
-
39
- const page = filtered.slice(request.skip, request.skip + request.take);
40
-
41
- return of({ data: page, totalCount: filtered.length });
42
- }
43
- }
1
+ import { Observable, of } from 'rxjs';
2
+ import { SearchableSelectDataSource, PagedRequest, PagedResponse } from './mat-list-shared.model';
3
+
4
+ /**
5
+ * A ready-made data source backed by a static in-memory array.
6
+ *
7
+ * Use this when you want to demo or test the component without a real API:
8
+ *
9
+ * ```ts
10
+ * const dataSource = new MockSearchableSelectDataSource([
11
+ * { id: 1, name: 'Paris' },
12
+ * { id: 2, name: 'London' },
13
+ * ]);
14
+ *
15
+ * this.config = {
16
+ * dataSource,
17
+ * option: { ... },
18
+ * mode: 'create',
19
+ * };
20
+ * ```
21
+ *
22
+ * Search is applied across all string-coercible fields of each item.
23
+ * Pagination uses the `skip` / `take` values from the request.
24
+ */
25
+ export class MockSearchableSelectDataSource implements SearchableSelectDataSource {
26
+ constructor(private readonly items: Record<string, unknown>[]) {}
27
+
28
+ getAll(request: PagedRequest): Observable<PagedResponse<Record<string, unknown>>> {
29
+ const term = request.searchString?.toLowerCase() ?? '';
30
+
31
+ const filtered = term
32
+ ? this.items.filter(item =>
33
+ Object.values(item).some(value =>
34
+ String(value ?? '').toLowerCase().includes(term)
35
+ )
36
+ )
37
+ : this.items;
38
+
39
+ const page = filtered.slice(request.skip, request.skip + request.take);
40
+
41
+ return of({ data: page, totalCount: filtered.length });
42
+ }
43
+ }
@@ -1,52 +1,52 @@
1
- import { Directive, EventEmitter, Input, OnDestroy, Output, Self } from '@angular/core';
2
- import { MatSelect } from '@angular/material/select';
3
- import { Subscription } from 'rxjs';
4
-
5
- @Directive({
6
- selector: 'mat-select[matSelectInfiniteScroll]',
7
- standalone: true,
8
- })
9
- export class MatSelectInfiniteScrollDirective implements OnDestroy {
10
- @Input() complete = false;
11
- @Input() threshold = 0.8;
12
- @Output() infiniteScroll = new EventEmitter<void>();
13
-
14
- private subscription = new Subscription();
15
- private boundOnScroll = this.onScroll.bind(this);
16
-
17
- constructor(@Self() private matSelect: MatSelect) {
18
- this.subscription.add(
19
- this.matSelect.openedChange.subscribe((isOpen: boolean) => {
20
- if (isOpen) {
21
- setTimeout(() => this.attachScrollListener());
22
- } else {
23
- this.detachScrollListener();
24
- }
25
- })
26
- );
27
- }
28
-
29
- private attachScrollListener(): void {
30
- const panel = this.matSelect.panel?.nativeElement as HTMLElement | undefined;
31
- panel?.addEventListener('scroll', this.boundOnScroll);
32
- }
33
-
34
- private detachScrollListener(): void {
35
- const panel = this.matSelect.panel?.nativeElement as HTMLElement | undefined;
36
- panel?.removeEventListener('scroll', this.boundOnScroll);
37
- }
38
-
39
- private onScroll(event: Event): void {
40
- if (this.complete) return;
41
- const el = event.target as HTMLElement;
42
- const scrollRatio = (el.scrollTop + el.clientHeight) / el.scrollHeight;
43
- if (scrollRatio >= this.threshold) {
44
- this.infiniteScroll.emit();
45
- }
46
- }
47
-
48
- ngOnDestroy(): void {
49
- this.detachScrollListener();
50
- this.subscription.unsubscribe();
51
- }
52
- }
1
+ import { Directive, EventEmitter, Input, OnDestroy, Output, Self } from '@angular/core';
2
+ import { MatSelect } from '@angular/material/select';
3
+ import { Subscription } from 'rxjs';
4
+
5
+ @Directive({
6
+ selector: 'mat-select[matSelectInfiniteScroll]',
7
+ standalone: true,
8
+ })
9
+ export class MatSelectInfiniteScrollDirective implements OnDestroy {
10
+ @Input() complete = false;
11
+ @Input() threshold = 0.8;
12
+ @Output() infiniteScroll = new EventEmitter<void>();
13
+
14
+ private subscription = new Subscription();
15
+ private boundOnScroll = this.onScroll.bind(this);
16
+
17
+ constructor(@Self() private matSelect: MatSelect) {
18
+ this.subscription.add(
19
+ this.matSelect.openedChange.subscribe((isOpen: boolean) => {
20
+ if (isOpen) {
21
+ setTimeout(() => this.attachScrollListener());
22
+ } else {
23
+ this.detachScrollListener();
24
+ }
25
+ })
26
+ );
27
+ }
28
+
29
+ private attachScrollListener(): void {
30
+ const panel = this.matSelect.panel?.nativeElement as HTMLElement | undefined;
31
+ panel?.addEventListener('scroll', this.boundOnScroll);
32
+ }
33
+
34
+ private detachScrollListener(): void {
35
+ const panel = this.matSelect.panel?.nativeElement as HTMLElement | undefined;
36
+ panel?.removeEventListener('scroll', this.boundOnScroll);
37
+ }
38
+
39
+ private onScroll(event: Event): void {
40
+ if (this.complete) return;
41
+ const el = event.target as HTMLElement;
42
+ const scrollRatio = (el.scrollTop + el.clientHeight) / el.scrollHeight;
43
+ if (scrollRatio >= this.threshold) {
44
+ this.infiniteScroll.emit();
45
+ }
46
+ }
47
+
48
+ ngOnDestroy(): void {
49
+ this.detachScrollListener();
50
+ this.subscription.unsubscribe();
51
+ }
52
+ }
package/.editorconfig DELETED
@@ -1,16 +0,0 @@
1
- # Editor configuration, see https://editorconfig.org
2
- root = true
3
-
4
- [*]
5
- charset = utf-8
6
- indent_style = space
7
- indent_size = 2
8
- insert_final_newline = true
9
- trim_trailing_whitespace = true
10
-
11
- [*.ts]
12
- quote_type = single
13
-
14
- [*.md]
15
- max_line_length = off
16
- trim_trailing_whitespace = false
@@ -1,4 +0,0 @@
1
- {
2
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3
- "recommendations": ["angular.ng-template"]
4
- }