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.
- package/README.md +19 -127
- package/package.json +12 -44
- package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.component.html +75 -75
- package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.mock.ts +43 -43
- package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-select-infinite-scroll.directive.ts +52 -52
- package/.editorconfig +0 -16
- package/.vscode/extensions.json +0 -4
- package/.vscode/launch.json +0 -20
- package/.vscode/tasks.json +0 -42
- package/LICENSE +0 -21
- package/angular.json +0 -151
- package/projects/demo/public/favicon.ico +0 -0
- package/projects/demo/src/app/app.config.ts +0 -9
- package/projects/demo/src/app/app.html +0 -118
- package/projects/demo/src/app/app.scss +0 -64
- package/projects/demo/src/app/app.ts +0 -167
- package/projects/demo/src/index.html +0 -16
- package/projects/demo/src/main.ts +0 -6
- package/projects/demo/src/styles.scss +0 -31
- package/projects/demo/tsconfig.app.json +0 -17
- package/projects/demo/tsconfig.spec.json +0 -15
- package/projects/ngx-mat-searchable-select/README.md +0 -262
- package/projects/ngx-mat-searchable-select/package.json +0 -12
- package/tsconfig.json +0 -39
- /package/{projects/ngx-mat-searchable-select/karma.conf.js → karma.conf.js} +0 -0
- /package/{projects/ngx-mat-searchable-select/ng-package.json → ng-package.json} +0 -0
- /package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.component.spec.ts +0 -0
- /package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.component.ts +0 -0
- /package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.model.ts +0 -0
- /package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-list-shared.service.spec.ts +0 -0
- /package/{projects/ngx-mat-searchable-select/src → src}/public-api.ts +0 -0
- /package/{projects/ngx-mat-searchable-select/src → src}/test.ts +0 -0
- /package/{projects/ngx-mat-searchable-select/tsconfig.lib.json → tsconfig.lib.json} +0 -0
- /package/{projects/ngx-mat-searchable-select/tsconfig.lib.prod.json → tsconfig.lib.prod.json} +0 -0
- /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',
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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": "
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|
package/{projects/ngx-mat-searchable-select/src → src}/lib/mat-select-infinite-scroll.directive.ts
RENAMED
|
@@ -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
|
package/.vscode/extensions.json
DELETED