@smallpearl/ngx-helper 0.32.9 → 0.32.11
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/assets/i18n/sp-mat-select-entity/en.json +1 -1
- package/assets/i18n/sp-mat-select-entity/zh-hant.json +1 -1
- package/entities/index.d.ts +2 -0
- package/entities/src/paged-loader.d.ts +212 -0
- package/entities/src/paginator.d.ts +87 -0
- package/fesm2022/smallpearl-ngx-helper-entities.mjs +545 -0
- package/fesm2022/smallpearl-ngx-helper-entities.mjs.map +1 -0
- package/fesm2022/smallpearl-ngx-helper-mat-entity-list.mjs.map +1 -1
- package/fesm2022/smallpearl-ngx-helper-mat-select-entity.mjs +476 -471
- package/fesm2022/smallpearl-ngx-helper-mat-select-entity.mjs.map +1 -1
- package/fesm2022/smallpearl-ngx-helper-mat-select-infinite-scroll.mjs +138 -0
- package/fesm2022/smallpearl-ngx-helper-mat-select-infinite-scroll.mjs.map +1 -0
- package/mat-entity-list/src/mat-entity-list-types.d.ts +62 -0
- package/mat-select-entity/src/mat-select-entity.component.d.ts +71 -104
- package/mat-select-infinite-scroll/index.d.ts +2 -0
- package/mat-select-infinite-scroll/src/mat-select-infinite-scroll.directive.d.ts +19 -0
- package/mat-select-infinite-scroll/src/mat-select-infinite-scroll.service.d.ts +25 -0
- package/package.json +20 -11
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
|
2
2
|
import * as i1 from '@angular/common';
|
|
3
3
|
import { CommonModule, NgTemplateOutlet } from '@angular/common';
|
|
4
|
-
import { HttpContextToken,
|
|
4
|
+
import { HttpContextToken, HttpContext } from '@angular/common/http';
|
|
5
5
|
import * as i0 from '@angular/core';
|
|
6
|
-
import { input, EventEmitter, computed, inject, ChangeDetectorRef, ElementRef, Component, ChangeDetectionStrategy,
|
|
6
|
+
import { input, EventEmitter, computed, viewChild, inject, ChangeDetectorRef, ElementRef, Component, ChangeDetectionStrategy, Output, HostBinding, Input } from '@angular/core';
|
|
7
7
|
import * as i2 from '@angular/forms';
|
|
8
8
|
import { NgControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
9
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
9
10
|
import { MAT_FORM_FIELD, MatFormFieldControl } from '@angular/material/form-field';
|
|
11
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
10
12
|
import * as i3 from '@angular/material/select';
|
|
11
13
|
import { MatSelect, MatSelectModule } from '@angular/material/select';
|
|
12
14
|
import * as i5 from '@jsverse/transloco';
|
|
13
15
|
import { TranslocoService, provideTranslocoScope, TranslocoModule } from '@jsverse/transloco';
|
|
14
|
-
import {
|
|
16
|
+
import { selectAllEntities, upsertEntities, hasEntity, getEntity } from '@ngneat/elf-entities';
|
|
17
|
+
import { SPPagedEntityLoader } from '@smallpearl/ngx-helper/entities';
|
|
18
|
+
import { MatSelectInfiniteScrollDirective } from '@smallpearl/ngx-helper/mat-select-infinite-scroll';
|
|
15
19
|
import * as i6 from 'ngx-mat-select-search';
|
|
16
20
|
import { NgxMatSelectSearchModule } from 'ngx-mat-select-search';
|
|
17
|
-
import {
|
|
18
|
-
import { Subject, BehaviorSubject, combineLatest, debounceTime, takeUntil, switchMap, of, tap } from 'rxjs';
|
|
21
|
+
import { Subject, startWith, distinctUntilChanged, debounceTime, tap, combineLatest, takeUntil } from 'rxjs';
|
|
19
22
|
import * as i4 from '@angular/material/core';
|
|
20
23
|
|
|
21
24
|
const SP_MAT_SELECT_ENTITY_HTTP_CONTEXT = new HttpContextToken(() => ({
|
|
@@ -23,6 +26,68 @@ const SP_MAT_SELECT_ENTITY_HTTP_CONTEXT = new HttpContextToken(() => ({
|
|
|
23
26
|
entityNamePlural: '',
|
|
24
27
|
endpoint: '',
|
|
25
28
|
}));
|
|
29
|
+
// Default paginator implementation. This can handle dynamic-rest and DRF
|
|
30
|
+
// native pagination schemes. It also has a fallback to handle response conists
|
|
31
|
+
// of an array of entities.
|
|
32
|
+
class DefaultPaginator {
|
|
33
|
+
getRequestPageParams(endpoint, page, pageSize) {
|
|
34
|
+
return {
|
|
35
|
+
page: page + 1,
|
|
36
|
+
pageSize,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
parseRequestResponse(entityName, entityNamePlural, endpoint, params, resp) {
|
|
40
|
+
if (Array.isArray(resp)) {
|
|
41
|
+
return {
|
|
42
|
+
total: resp.length,
|
|
43
|
+
entities: resp,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (typeof resp === 'object') {
|
|
47
|
+
const keys = Object.keys(resp);
|
|
48
|
+
// Handle dynamic-rest sideloaded response
|
|
49
|
+
// Rudimentary sideloaded response support. This should work for most
|
|
50
|
+
// of the sideloaded responses where the main entities are stored
|
|
51
|
+
// under the plural entity name key and resp['meta'] object contains
|
|
52
|
+
// the total count.
|
|
53
|
+
if (keys.includes(entityNamePlural) &&
|
|
54
|
+
Array.isArray(resp[entityNamePlural])) {
|
|
55
|
+
let total = resp[entityNamePlural].length;
|
|
56
|
+
if (keys.includes('meta') &&
|
|
57
|
+
typeof resp['meta'] === 'object' &&
|
|
58
|
+
typeof resp['meta']['total'] === 'number') {
|
|
59
|
+
total = resp['meta']['total'];
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
total,
|
|
63
|
+
entities: resp[entityNamePlural],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Handle django-rest-framework style response
|
|
67
|
+
if (keys.includes('results') && Array.isArray(resp['results'])) {
|
|
68
|
+
let total = resp['results'].length;
|
|
69
|
+
if (keys.includes('count') && typeof resp['count'] === 'number') {
|
|
70
|
+
total = resp['count'];
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
total,
|
|
74
|
+
entities: resp['results'],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Finally, look for "items" key
|
|
78
|
+
if (keys.includes('items') && Array.isArray(resp['items'])) {
|
|
79
|
+
return {
|
|
80
|
+
total: resp['items'].length,
|
|
81
|
+
entities: resp['items'],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
total: 0,
|
|
87
|
+
entities: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
26
91
|
/**
|
|
27
92
|
* This is a generic component to display a <mat-select> for a FK field
|
|
28
93
|
* where the select's options are dynamically loaded from the server using
|
|
@@ -32,7 +97,7 @@ const SP_MAT_SELECT_ENTITY_HTTP_CONTEXT = new HttpContextToken(() => ({
|
|
|
32
97
|
* be set to the object's `id` property. By default 'id' is used as its id,
|
|
33
98
|
* but this can be customized by specifying the `idKey' property value.
|
|
34
99
|
*/
|
|
35
|
-
class SPMatSelectEntityComponent {
|
|
100
|
+
class SPMatSelectEntityComponent extends SPPagedEntityLoader {
|
|
36
101
|
// We cache the entities that we fetch from remote here. Cache is indexed
|
|
37
102
|
// by the endpoint. Each endpoint also keeps a refCount, which is incremented
|
|
38
103
|
// for each instance of the component using the same endpoint. When this
|
|
@@ -40,104 +105,49 @@ class SPMatSelectEntityComponent {
|
|
|
40
105
|
//
|
|
41
106
|
// This mechanism is to suppress multiple fetches from the remote from the
|
|
42
107
|
// same endpoint as that can occur if a form has multiple instances of
|
|
43
|
-
// this component
|
|
44
|
-
static _entitiesCache = new Map
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* Entity idKey, if idKey is different from the default 'id'.
|
|
62
|
-
*/
|
|
63
|
-
idKey = 'id';
|
|
64
|
-
/**
|
|
65
|
-
* URL of the remote from where entities are to be loaded.
|
|
66
|
-
* This won't be used if `loadFromRemoteFn` is specified.
|
|
67
|
-
*/
|
|
68
|
-
url;
|
|
69
|
-
/**
|
|
70
|
-
* Parameters to be added to the HTTP request to retrieve data from
|
|
71
|
-
* remote. This won't be used if `loadFromRemoteFn` is specified.
|
|
72
|
-
*/
|
|
73
|
-
httpParams;
|
|
74
|
-
/**
|
|
75
|
-
* Function to load entities from remote.
|
|
76
|
-
*/
|
|
77
|
-
loadFromRemoteFn;
|
|
78
|
-
inlineNew = false;
|
|
79
|
-
/**
|
|
80
|
-
* Entity name, that is used to form the "New { item }" menu item if
|
|
81
|
-
* inlineNew=true. This is also used as the key of the object in GET response
|
|
82
|
-
* if the reponse JSON is not an array and rather an object, where the values
|
|
83
|
-
* are stored indexed by the server model name. For eg:-
|
|
84
|
-
*
|
|
85
|
-
* {
|
|
86
|
-
* 'customers': [
|
|
87
|
-
* {...},
|
|
88
|
-
* {...},
|
|
89
|
-
* {...},
|
|
90
|
-
* ]
|
|
91
|
-
* }
|
|
92
|
-
*/
|
|
93
|
-
entityName;
|
|
108
|
+
// this component with the same url.
|
|
109
|
+
// static _entitiesCache = new Map<
|
|
110
|
+
// string,
|
|
111
|
+
// { refCount: number; entities: Array<any> }
|
|
112
|
+
// >();
|
|
113
|
+
//**** OPTIONAL ATTRIBUTES ****//
|
|
114
|
+
// Entity label function - function that takes an entity object and returns
|
|
115
|
+
// a string label for it. If not specified, a default label function is used
|
|
116
|
+
// that returns the value of 'name' or 'label' or 'title' property. If
|
|
117
|
+
// none of these properties are present, the entity's id is returned as
|
|
118
|
+
// string.
|
|
119
|
+
labelFn = input();
|
|
120
|
+
// Entity filter function - return a boolean if the entity is to be included
|
|
121
|
+
// in the filtered entities list.
|
|
122
|
+
filterFn = input();
|
|
123
|
+
// Set to true to show "Add { item }" option in the select dropdown.
|
|
124
|
+
// Selecting this option, will emit `createNewItemSelected` event.
|
|
125
|
+
inlineNew = input(false);
|
|
94
126
|
// Set to true to allow multiple option selection. The returned value
|
|
95
127
|
// would be an array of entity ids.
|
|
96
|
-
multiple = false;
|
|
97
|
-
/*
|
|
98
|
-
Whether to group options using <mat-optgroup></mat-optgroup>.
|
|
99
|
-
If set to true, the response from the server should be an array of
|
|
100
|
-
groups of TEntity objects, where each object is of the form:
|
|
101
|
-
[
|
|
102
|
-
{
|
|
103
|
-
id: <id>,
|
|
104
|
-
name|label: <>,
|
|
105
|
-
items|<plural_entityName>|<custom_key>: [
|
|
106
|
-
TEntity,
|
|
107
|
-
...
|
|
108
|
-
]
|
|
109
|
-
},
|
|
110
|
-
...
|
|
111
|
-
]
|
|
112
|
-
*/
|
|
113
|
-
group = false;
|
|
128
|
+
multiple = input(false);
|
|
114
129
|
/**
|
|
115
|
-
* The
|
|
116
|
-
*
|
|
117
|
-
*
|
|
130
|
+
* The entity key name that is used to classify entities into groups.
|
|
131
|
+
* Entities with the same key value will be grouped together. If this is
|
|
132
|
+
* specified, grouping will be enabled.
|
|
133
|
+
* @see groupByFn
|
|
118
134
|
*/
|
|
119
|
-
groupOptionsKey;
|
|
135
|
+
groupOptionsKey = input();
|
|
120
136
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
137
|
+
* A function that takes a TEntity and returns the group id (string)
|
|
138
|
+
* that the entity belongs to. If this is specified, grouping of entities
|
|
139
|
+
* in the select will be enabled. This takes precedence over
|
|
140
|
+
* `groupOptionsKey`.
|
|
141
|
+
* @see groupOptionsKey
|
|
125
142
|
*/
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Sideload data key name.
|
|
129
|
-
*/
|
|
130
|
-
sideloadDataKey = input();
|
|
131
|
-
/**
|
|
132
|
-
* Parser function to return the list of entities from the GET response.
|
|
133
|
-
*/
|
|
134
|
-
responseParserFn = input();
|
|
143
|
+
groupByFn = input();
|
|
135
144
|
selectionChange = new EventEmitter();
|
|
136
145
|
createNewItemSelected = new EventEmitter();
|
|
137
|
-
//
|
|
146
|
+
// i18n localization support toallow per component customization of
|
|
147
|
+
// some strings used.
|
|
138
148
|
searchText = input();
|
|
139
149
|
notFoundText = input();
|
|
140
|
-
|
|
150
|
+
createNewText = input();
|
|
141
151
|
controlType = 'sp-mat-select-entity';
|
|
142
152
|
/**
|
|
143
153
|
* Template for the option label. If not provided, the default label
|
|
@@ -149,7 +159,7 @@ class SPMatSelectEntityComponent {
|
|
|
149
159
|
* ```
|
|
150
160
|
* <sp-mat-select-entity
|
|
151
161
|
* [url]="'/api/v1/customers/'"
|
|
152
|
-
* [
|
|
162
|
+
* [labelFn]="entity => entity.name"
|
|
153
163
|
* [optionLabelTemplate]="optionLabelTemplate"
|
|
154
164
|
* ></sp-mat-select-entity>
|
|
155
165
|
* <ng-template #optionLabelTemplate let-entity>
|
|
@@ -158,25 +168,44 @@ class SPMatSelectEntityComponent {
|
|
|
158
168
|
* ```
|
|
159
169
|
*/
|
|
160
170
|
optionLabelTemplate = input();
|
|
171
|
+
// a computed version of labelFn that provides a default implementation
|
|
161
172
|
_entityLabelFn = computed(() => {
|
|
162
|
-
const fn = this.
|
|
173
|
+
const fn = this.labelFn();
|
|
163
174
|
if (fn) {
|
|
164
175
|
return fn;
|
|
165
176
|
}
|
|
166
177
|
return (entity) => {
|
|
167
178
|
return (entity['name'] ||
|
|
168
179
|
entity['label'] ||
|
|
169
|
-
|
|
180
|
+
entity['title'] ||
|
|
181
|
+
String(entity[this.idKey()]));
|
|
170
182
|
};
|
|
171
183
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return this.entityName ? plural(camelCase(this.entityName)) : 'results';
|
|
184
|
+
// Whether to group options. Grouping is enabled when either groupOptionsKey
|
|
185
|
+
// or groupByFn is specified.
|
|
186
|
+
_group = computed(() => {
|
|
187
|
+
return !!this.groupOptionsKey() || !!this.groupByFn();
|
|
177
188
|
});
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
_groupEntitiesKey = computed(() => {
|
|
190
|
+
const groupOptionsKey = this.groupOptionsKey();
|
|
191
|
+
return groupOptionsKey
|
|
192
|
+
? groupOptionsKey
|
|
193
|
+
: this.entityName()
|
|
194
|
+
? this._pluralEntityName()
|
|
195
|
+
: 'items';
|
|
196
|
+
});
|
|
197
|
+
// For the global paginator. We'll abstract this into an independent
|
|
198
|
+
// configuration that can be shared across both mat-entity-list and
|
|
199
|
+
// mat-select-entity later.
|
|
200
|
+
// entityListConfig = getEntityListConfig();
|
|
201
|
+
// protected _paginator = computed<SPMatEntityListPaginator>(() => {
|
|
202
|
+
// const paginator = this.paginator();
|
|
203
|
+
// const entityListConfigPaginator = this.entityListConfig
|
|
204
|
+
// ?.paginator as SPMatEntityListPaginator;
|
|
205
|
+
// return paginator
|
|
206
|
+
// ? paginator
|
|
207
|
+
// : entityListConfigPaginator ?? new DefaultPaginator();
|
|
208
|
+
// });
|
|
180
209
|
stateChanges = new Subject();
|
|
181
210
|
focused = false;
|
|
182
211
|
touched = false;
|
|
@@ -190,55 +219,110 @@ class SPMatSelectEntityComponent {
|
|
|
190
219
|
lastSelectValue;
|
|
191
220
|
searching = false;
|
|
192
221
|
filterStr = '';
|
|
193
|
-
filter$ = new
|
|
194
|
-
// ControlValueAccessor
|
|
222
|
+
filter$ = new Subject();
|
|
223
|
+
// ControlValueAccessor callbacks
|
|
195
224
|
onChanged = (_) => { };
|
|
196
225
|
onTouched = () => { };
|
|
197
|
-
matSelect;
|
|
226
|
+
// @ViewChild(MatSelect) matSelect!: MatSelect;
|
|
227
|
+
matSelect = viewChild(MatSelect);
|
|
198
228
|
filteredValues = new Subject();
|
|
199
229
|
filteredGroupedValues = new Subject();
|
|
200
230
|
destroy = new Subject();
|
|
201
|
-
loaded = false;
|
|
202
|
-
load$ = new BehaviorSubject(false);
|
|
203
231
|
static nextId = 0;
|
|
204
232
|
id = `sp-select-entity-${SPMatSelectEntityComponent.nextId++}`;
|
|
205
233
|
_placeholder;
|
|
206
|
-
http = inject(HttpClient);
|
|
234
|
+
//protected http = inject(HttpClient);
|
|
207
235
|
cdr = inject(ChangeDetectorRef);
|
|
208
236
|
_elementRef = inject((ElementRef));
|
|
209
237
|
_formField = inject(MAT_FORM_FIELD, { optional: true });
|
|
210
238
|
ngControl = inject(NgControl, { optional: true });
|
|
211
239
|
transloco = inject(TranslocoService);
|
|
240
|
+
// pagedEntityLoader!: SPPagedEntityLoader<TEntity, IdKey>;
|
|
212
241
|
constructor() {
|
|
242
|
+
super();
|
|
213
243
|
if (this.ngControl != null) {
|
|
214
244
|
this.ngControl.valueAccessor = this;
|
|
215
245
|
}
|
|
216
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Conditions for loading entities:
|
|
249
|
+
*
|
|
250
|
+
* 1. When the select is opened, if entities have not already been loaded.
|
|
251
|
+
* 2. When the search string changes.
|
|
252
|
+
* 3. When the scroll reaches the bottom and more entities are available
|
|
253
|
+
* to be loaded.
|
|
254
|
+
*
|
|
255
|
+
* We need to create an 'observer-loop' that can handle the above.
|
|
256
|
+
*/
|
|
217
257
|
ngOnInit() {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
258
|
+
// this.pagedEntityLoader = new SPPagedEntityLoader<TEntity, IdKey>(
|
|
259
|
+
// this.entityName(),
|
|
260
|
+
// this.url(),
|
|
261
|
+
// this.http,
|
|
262
|
+
// this.pageSize(),
|
|
263
|
+
// this._paginator(),
|
|
264
|
+
// this.searchParamName(),
|
|
265
|
+
// this.idKey(),
|
|
266
|
+
// this._pluralEntityName(),
|
|
267
|
+
// undefined,
|
|
268
|
+
// this.httpParams()
|
|
269
|
+
// );
|
|
270
|
+
this.startLoader();
|
|
271
|
+
// A rudimentary mechanism to detect which of the two observables
|
|
272
|
+
// emitted the latest value. We reset this array to 'false' after
|
|
273
|
+
// processing every combined emission.
|
|
274
|
+
const emittedObservable = [false, false];
|
|
275
|
+
const store$ = this.store.pipe(selectAllEntities());
|
|
276
|
+
const filter$ = this.filter$.pipe(startWith(''), distinctUntilChanged(), debounceTime(400));
|
|
277
|
+
const emittedStatusObservable = (obs, index) => obs.pipe(tap(() => (emittedObservable[index] = true)));
|
|
278
|
+
// We need to determine if the emission is owing to a change in
|
|
279
|
+
// filterStr or a change in the entities in pagedEntityLoader.store$.
|
|
280
|
+
//
|
|
281
|
+
// 1. If entities in pagedEntityLoader.store$ have changed, we just need
|
|
282
|
+
// to filter the entities in local store using the current filterStr.
|
|
283
|
+
// 2. If filterStr has changed, there are two cases to handle:-
|
|
284
|
+
// a. If all entities have been loaded, we don't need to reload
|
|
285
|
+
// entities. Instead we just have to filter the entities in
|
|
286
|
+
// local store using the filterStr.
|
|
287
|
+
// b. If all entities have not been loaded, we trigger a server
|
|
288
|
+
// load with the new filterStr as the search param.
|
|
289
|
+
//
|
|
290
|
+
// The following logic implements the above.
|
|
291
|
+
combineLatest([
|
|
292
|
+
emittedStatusObservable(store$, 0),
|
|
293
|
+
emittedStatusObservable(filter$, 1),
|
|
294
|
+
])
|
|
295
|
+
.pipe(takeUntil(this.destroy), tap(([entities, filterStr]) => {
|
|
296
|
+
if (emittedObservable.every((eo) => eo)) {
|
|
297
|
+
// initial emission. This can be combined with the case immediately
|
|
298
|
+
// below it. But we keep it separate for clarity.
|
|
299
|
+
emittedObservable[0] = emittedObservable[1] = false;
|
|
300
|
+
this.filterEntities(entities, filterStr);
|
|
224
301
|
}
|
|
225
|
-
else {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}), tap(() => {
|
|
229
|
-
if (this.group) {
|
|
230
|
-
this.filterGroupedValues(this.filterStr);
|
|
302
|
+
else if (emittedObservable[0]) {
|
|
303
|
+
emittedObservable[0] = false;
|
|
304
|
+
this.filterEntities(entities, filterStr);
|
|
231
305
|
}
|
|
232
306
|
else {
|
|
233
|
-
|
|
307
|
+
emittedObservable[1] = false;
|
|
308
|
+
if (this.allEntitiesLoaded()) {
|
|
309
|
+
this.filterEntities(entities, filterStr);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
this.setSearchParamValue(filterStr);
|
|
313
|
+
// This will cause an emission from store$ observable as the
|
|
314
|
+
// 'forceRefresh=true' arg causes the store to be reset.
|
|
315
|
+
this.loadNextPage(true);
|
|
316
|
+
}
|
|
234
317
|
}
|
|
235
|
-
this.cdr.detectChanges();
|
|
236
318
|
}))
|
|
237
319
|
.subscribe();
|
|
238
320
|
}
|
|
239
321
|
ngOnDestroy() {
|
|
240
322
|
this.destroy.next();
|
|
241
|
-
this.
|
|
323
|
+
this.destroy.complete();
|
|
324
|
+
this.stopLoader();
|
|
325
|
+
// this.removeFromCache();
|
|
242
326
|
this.stateChanges.complete();
|
|
243
327
|
}
|
|
244
328
|
ngAfterViewInit() {
|
|
@@ -250,11 +334,11 @@ class SPMatSelectEntityComponent {
|
|
|
250
334
|
// this.required = true;
|
|
251
335
|
// }
|
|
252
336
|
// }
|
|
337
|
+
// load first page
|
|
338
|
+
// this.loadMoreEntities();
|
|
253
339
|
}
|
|
254
340
|
addEntity(entity) {
|
|
255
|
-
this.
|
|
256
|
-
// So that the newly added entity will be added to the <mat-option> list.
|
|
257
|
-
this.filterValues(this.filterStr);
|
|
341
|
+
this.store.update(upsertEntities(entity));
|
|
258
342
|
this.cdr.detectChanges();
|
|
259
343
|
}
|
|
260
344
|
get selectTriggerValue() {
|
|
@@ -262,7 +346,7 @@ class SPMatSelectEntityComponent {
|
|
|
262
346
|
const firstSelected = Array.isArray(this.selectValue)
|
|
263
347
|
? this.selectValue[0]
|
|
264
348
|
: this.selectValue;
|
|
265
|
-
const selectedEntity = this.
|
|
349
|
+
const selectedEntity = this.getEntity(firstSelected);
|
|
266
350
|
return selectedEntity ? this._entityLabelFn()(selectedEntity) : '';
|
|
267
351
|
}
|
|
268
352
|
return '';
|
|
@@ -273,14 +357,16 @@ class SPMatSelectEntityComponent {
|
|
|
273
357
|
: [];
|
|
274
358
|
}
|
|
275
359
|
entityId(entity) {
|
|
276
|
-
return entity[this.idKey];
|
|
360
|
+
return entity[this.idKey()];
|
|
277
361
|
}
|
|
278
362
|
writeValue(entityId) {
|
|
363
|
+
const store = this.store;
|
|
364
|
+
const entities = this.getEntities();
|
|
279
365
|
if (Array.isArray(entityId)) {
|
|
280
|
-
if (this.multiple) {
|
|
366
|
+
if (this.multiple()) {
|
|
281
367
|
const selectedValues = [];
|
|
282
368
|
entityId.forEach((id) => {
|
|
283
|
-
if (
|
|
369
|
+
if (store.query(hasEntity(id))) {
|
|
284
370
|
selectedValues.push(id);
|
|
285
371
|
}
|
|
286
372
|
});
|
|
@@ -289,11 +375,12 @@ class SPMatSelectEntityComponent {
|
|
|
289
375
|
}
|
|
290
376
|
}
|
|
291
377
|
else {
|
|
292
|
-
if (
|
|
378
|
+
if (store.query(hasEntity(entityId))) {
|
|
379
|
+
// if (this._entities.has(entityId)) {
|
|
293
380
|
this.selectValue = entityId;
|
|
294
381
|
if (this.filterStr) {
|
|
295
382
|
this.filterStr = '';
|
|
296
|
-
this.
|
|
383
|
+
// this.filterNonGroupedEntities(entities, this.filterStr);
|
|
297
384
|
}
|
|
298
385
|
this.cdr.detectChanges();
|
|
299
386
|
}
|
|
@@ -306,25 +393,10 @@ class SPMatSelectEntityComponent {
|
|
|
306
393
|
this.onTouched = fn;
|
|
307
394
|
}
|
|
308
395
|
get entities() {
|
|
309
|
-
return
|
|
396
|
+
return this.getEntities();
|
|
310
397
|
}
|
|
311
398
|
set entities(items) {
|
|
312
|
-
|
|
313
|
-
items.forEach((item) => {
|
|
314
|
-
this._entities.set(item[this.idKey], item);
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
this._groupedEntities = items;
|
|
319
|
-
this._groupedEntities.forEach((group) => {
|
|
320
|
-
const key = this.groupEntitiesKey();
|
|
321
|
-
const groupEntities = group[key];
|
|
322
|
-
group['__items__'] = groupEntities;
|
|
323
|
-
groupEntities.forEach((item) => {
|
|
324
|
-
this._entities.set(item[this.idKey], item);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
}
|
|
399
|
+
this.setEntities(items);
|
|
328
400
|
}
|
|
329
401
|
get value() {
|
|
330
402
|
return this.selectValue;
|
|
@@ -395,8 +467,9 @@ class SPMatSelectEntityComponent {
|
|
|
395
467
|
}
|
|
396
468
|
setDisabledState(isDisabled) {
|
|
397
469
|
this._disabled = isDisabled;
|
|
398
|
-
|
|
399
|
-
|
|
470
|
+
const matSelect = this.matSelect();
|
|
471
|
+
if (matSelect) {
|
|
472
|
+
matSelect.setDisabledState(isDisabled);
|
|
400
473
|
this.cdr.detectChanges();
|
|
401
474
|
}
|
|
402
475
|
}
|
|
@@ -405,8 +478,9 @@ class SPMatSelectEntityComponent {
|
|
|
405
478
|
// eventually selects 'New Item' option.
|
|
406
479
|
this.lastSelectValue = this.selectValue;
|
|
407
480
|
// If values have not been loaded from remote, trigger a load.
|
|
408
|
-
if (
|
|
409
|
-
|
|
481
|
+
if (this.totalEntitiesAtRemote() === 0) {
|
|
482
|
+
// first load
|
|
483
|
+
this.loadNextPage();
|
|
410
484
|
}
|
|
411
485
|
}
|
|
412
486
|
onSelectionChange(ev) {
|
|
@@ -415,7 +489,7 @@ class SPMatSelectEntityComponent {
|
|
|
415
489
|
this.selectValue = ev.value;
|
|
416
490
|
this.onTouched();
|
|
417
491
|
this.onChanged(ev.value);
|
|
418
|
-
const selectedEntities = ev.value.map((id) => this.
|
|
492
|
+
const selectedEntities = ev.value.map((id) => this.store.query(getEntity(id)));
|
|
419
493
|
this.selectionChange.emit(selectedEntities);
|
|
420
494
|
}
|
|
421
495
|
else {
|
|
@@ -423,7 +497,7 @@ class SPMatSelectEntityComponent {
|
|
|
423
497
|
this.selectValue = ev.value;
|
|
424
498
|
this.onTouched();
|
|
425
499
|
this.onChanged(ev.value);
|
|
426
|
-
this.selectionChange.emit(this.
|
|
500
|
+
this.selectionChange.emit(this.store.query(getEntity(ev.value)));
|
|
427
501
|
}
|
|
428
502
|
else {
|
|
429
503
|
// New Item activated, return value to previous value. We track
|
|
@@ -438,23 +512,45 @@ class SPMatSelectEntityComponent {
|
|
|
438
512
|
}
|
|
439
513
|
}
|
|
440
514
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
515
|
+
/**
|
|
516
|
+
* Wrapper to filter entities based on whether grouping is enabled or not.
|
|
517
|
+
* Calls one of the two filtering methods -- filterGroupedEntities() or
|
|
518
|
+
* filterNonGroupedEntities().
|
|
519
|
+
* @param entities
|
|
520
|
+
* @param filterStr
|
|
521
|
+
* @returns
|
|
522
|
+
*/
|
|
523
|
+
filterEntities(entities, filterStr) {
|
|
524
|
+
this.searching = true;
|
|
525
|
+
let retval;
|
|
526
|
+
if (this._group()) {
|
|
527
|
+
this.filterGroupedEntities(entities, filterStr);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
this.filterNonGroupedEntities(entities, filterStr);
|
|
446
531
|
}
|
|
532
|
+
this.searching = false;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Filters the entities based on the search string.
|
|
536
|
+
* @param search The search string to filter entities.
|
|
537
|
+
* @returns The number of entities in the filtered result set or undefined.
|
|
538
|
+
*/
|
|
539
|
+
filterNonGroupedEntities(entities, search) {
|
|
540
|
+
const searchLwr = search.toLocaleLowerCase();
|
|
447
541
|
if (!search) {
|
|
448
542
|
this.filteredValues.next(entities.slice());
|
|
449
543
|
}
|
|
450
544
|
else {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
545
|
+
const filteredEntities = entities.filter((member) => {
|
|
546
|
+
const filterFn = this.filterFn();
|
|
547
|
+
if (filterFn) {
|
|
548
|
+
return filterFn(member, search);
|
|
454
549
|
}
|
|
455
550
|
const labelFn = this._entityLabelFn();
|
|
456
551
|
return labelFn(member).toLocaleLowerCase().includes(searchLwr);
|
|
457
|
-
})
|
|
552
|
+
});
|
|
553
|
+
this.filteredValues.next(filteredEntities);
|
|
458
554
|
}
|
|
459
555
|
}
|
|
460
556
|
/**
|
|
@@ -464,205 +560,144 @@ class SPMatSelectEntityComponent {
|
|
|
464
560
|
* groups are to be included and within those groups, only entities whose
|
|
465
561
|
* label matches the search string are to be included in the result set.
|
|
466
562
|
* @param search
|
|
467
|
-
* @returns
|
|
563
|
+
* @returns number of groups in the filtered result set.
|
|
468
564
|
*/
|
|
469
|
-
|
|
565
|
+
filterGroupedEntities(entities, search) {
|
|
470
566
|
const searchLwr = search.toLocaleLowerCase();
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
567
|
+
// First filter entities by the search string, if it's specified
|
|
568
|
+
let filteredEntities;
|
|
475
569
|
if (!search) {
|
|
476
|
-
|
|
477
|
-
this.filteredGroupedValues.next(groupsCopy);
|
|
570
|
+
filteredEntities = entities;
|
|
478
571
|
}
|
|
479
572
|
else {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return { ...ge };
|
|
485
|
-
}
|
|
486
|
-
else {
|
|
487
|
-
const groupEntities = ge.__items__?.filter((e) => this._entityLabelFn()(e).toLocaleLowerCase().includes(searchLwr));
|
|
488
|
-
const ret = {
|
|
489
|
-
...ge,
|
|
490
|
-
};
|
|
491
|
-
ret['__items__'] = groupEntities ?? [];
|
|
492
|
-
return ret;
|
|
573
|
+
filteredEntities = entities.filter((member) => {
|
|
574
|
+
const filterFn = this.filterFn();
|
|
575
|
+
if (filterFn) {
|
|
576
|
+
return filterFn(member, search);
|
|
493
577
|
}
|
|
578
|
+
const labelFn = this._entityLabelFn();
|
|
579
|
+
return labelFn(member).toLocaleLowerCase().includes(searchLwr);
|
|
494
580
|
});
|
|
495
|
-
// filter out groups with no entities
|
|
496
|
-
// console.log(`Groups: ${JSON.stringify(groups)}`);
|
|
497
|
-
this.filteredGroupedValues.next(groups.filter((group) => Array.isArray(group[groupEntitiesKey]) &&
|
|
498
|
-
group['__items__'].length > 0));
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
loadFromRemote() {
|
|
502
|
-
if (!this.url && !this.loadFromRemoteFn) {
|
|
503
|
-
// If user had initialized entities, they will be dispalyed
|
|
504
|
-
// in the options list. If not, options would be empty.
|
|
505
|
-
return of(this.group ? this.groupEntities : this.entities);
|
|
506
|
-
}
|
|
507
|
-
let cacheKey;
|
|
508
|
-
let obs;
|
|
509
|
-
if (this.loadFromRemoteFn) {
|
|
510
|
-
obs = this.loadFromRemoteFn();
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
let params;
|
|
514
|
-
if (this.httpParams) {
|
|
515
|
-
params = new HttpParams({
|
|
516
|
-
fromString: this.httpParams.toString(),
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
params = new HttpParams();
|
|
521
|
-
}
|
|
522
|
-
params = params.set('paginate', false);
|
|
523
|
-
cacheKey = this.getCacheKey();
|
|
524
|
-
if (this.existsInCache()) {
|
|
525
|
-
obs = of(this.getFromCache());
|
|
526
|
-
}
|
|
527
|
-
else {
|
|
528
|
-
obs = this.http.get(this.url, {
|
|
529
|
-
context: this.getHttpReqContext(),
|
|
530
|
-
params,
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
return obs.pipe(tap((entities) => {
|
|
535
|
-
this.searching = false; // remote loading done, will hide the loading wheel
|
|
536
|
-
// Handle DRF paginated response
|
|
537
|
-
const responseParserFn = this.responseParserFn();
|
|
538
|
-
if (responseParserFn) {
|
|
539
|
-
entities = responseParserFn(entities);
|
|
540
|
-
}
|
|
541
|
-
else {
|
|
542
|
-
if (!Array.isArray(entities) &&
|
|
543
|
-
entities['results'] &&
|
|
544
|
-
Array.isArray(entities['results'])) {
|
|
545
|
-
entities = entities['results'];
|
|
546
|
-
}
|
|
547
|
-
else if (
|
|
548
|
-
// sideloaded response, where entities are usually provided in 'entityName'
|
|
549
|
-
this._sideloadDataKey() &&
|
|
550
|
-
!Array.isArray(entities) &&
|
|
551
|
-
typeof entities === 'object' &&
|
|
552
|
-
entities[this._sideloadDataKey()] &&
|
|
553
|
-
Array.isArray(entities[this._sideloadDataKey()])) {
|
|
554
|
-
entities = entities[this._sideloadDataKey()];
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (Array.isArray(entities)) {
|
|
558
|
-
this.entities = entities;
|
|
559
|
-
// if (this.group) {
|
|
560
|
-
// this._groupedEntities = entities as EntityGroup<TEntity>[];
|
|
561
|
-
// } else {
|
|
562
|
-
// this.entities = entities;
|
|
563
|
-
// }
|
|
564
|
-
}
|
|
565
|
-
this.loaded = true;
|
|
566
|
-
this.addToCache(entities);
|
|
567
|
-
this.cdr.detectChanges();
|
|
568
|
-
}));
|
|
569
|
-
}
|
|
570
|
-
groupLabel(group) {
|
|
571
|
-
if (this.groupLabelFn) {
|
|
572
|
-
return this.groupLabelFn(group);
|
|
573
|
-
}
|
|
574
|
-
const standardLabelFields = ['name', 'label', 'desc', 'description'];
|
|
575
|
-
for (let index = 0; index < standardLabelFields.length; index++) {
|
|
576
|
-
const labelField = standardLabelFields[index];
|
|
577
|
-
if (group[labelField]) {
|
|
578
|
-
return group[labelField];
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return `Group ${String(group.id)}`;
|
|
582
|
-
}
|
|
583
|
-
groupEntities(group) {
|
|
584
|
-
const key = this.groupEntitiesKey();
|
|
585
|
-
console.log(`groupEntities - group: ${JSON.stringify(group)}, key: ${key}`);
|
|
586
|
-
return group[this.groupEntitiesKey()] ?? [];
|
|
587
|
-
}
|
|
588
|
-
groupEntitiesKey() {
|
|
589
|
-
return this.groupOptionsKey
|
|
590
|
-
? this.groupOptionsKey
|
|
591
|
-
: this.entityName
|
|
592
|
-
? plural(this.entityName.toLocaleLowerCase())
|
|
593
|
-
: 'items';
|
|
594
|
-
}
|
|
595
|
-
existsInCache() {
|
|
596
|
-
const cacheKey = this.getCacheKey();
|
|
597
|
-
if (cacheKey) {
|
|
598
|
-
return SPMatSelectEntityComponent._entitiesCache.has(cacheKey);
|
|
599
|
-
}
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
getCacheKey() {
|
|
603
|
-
if (!this.loadFromRemoteFn) {
|
|
604
|
-
let params;
|
|
605
|
-
if (this.httpParams) {
|
|
606
|
-
params = new HttpParams({
|
|
607
|
-
fromString: this.httpParams.toString(),
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
params = new HttpParams();
|
|
612
|
-
}
|
|
613
|
-
// params = params.set('paginate', false)
|
|
614
|
-
return `${this.url}?${params.toString()}`;
|
|
615
|
-
}
|
|
616
|
-
return ''; // empty string evalutes to boolean(false)
|
|
617
|
-
}
|
|
618
|
-
getFromCache() {
|
|
619
|
-
const cacheKey = this.getCacheKey();
|
|
620
|
-
if (cacheKey && SPMatSelectEntityComponent._entitiesCache.has(cacheKey)) {
|
|
621
|
-
return SPMatSelectEntityComponent._entitiesCache.get(cacheKey)
|
|
622
|
-
?.entities;
|
|
623
|
-
}
|
|
624
|
-
return [];
|
|
625
|
-
}
|
|
626
|
-
addToCache(entities) {
|
|
627
|
-
const cacheKey = this.getCacheKey();
|
|
628
|
-
if (cacheKey) {
|
|
629
|
-
if (!SPMatSelectEntityComponent._entitiesCache.has(cacheKey)) {
|
|
630
|
-
SPMatSelectEntityComponent._entitiesCache.set(cacheKey, {
|
|
631
|
-
refCount: 0,
|
|
632
|
-
entities,
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
const cacheEntry = SPMatSelectEntityComponent._entitiesCache.get(cacheKey);
|
|
636
|
-
cacheEntry.refCount += 1;
|
|
637
581
|
}
|
|
582
|
+
this.filteredGroupedValues.next(this.groupEntities(filteredEntities));
|
|
638
583
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
584
|
+
/**
|
|
585
|
+
* Helper to arrange the given array of entities into groups based on the
|
|
586
|
+
* groupByFn or groupOptionsKey. groupByFn takes precedence over
|
|
587
|
+
* groupOptionsKey.
|
|
588
|
+
* @param entities
|
|
589
|
+
* @returns EntityGroup<TEntity>[]
|
|
590
|
+
*/
|
|
591
|
+
groupEntities(entities) {
|
|
592
|
+
let groupByFn;
|
|
593
|
+
if (this.groupByFn()) {
|
|
594
|
+
groupByFn = this.groupByFn();
|
|
595
|
+
}
|
|
596
|
+
else if (this.groupOptionsKey()) {
|
|
597
|
+
groupByFn = (entity) => {
|
|
598
|
+
const key = this.groupOptionsKey();
|
|
599
|
+
return entity[key] ?? '???';
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const groupedEntitiesMap = new Map();
|
|
603
|
+
entities.forEach((entity) => {
|
|
604
|
+
const groupId = groupByFn(entity);
|
|
605
|
+
if (!groupedEntitiesMap.has(groupId)) {
|
|
606
|
+
groupedEntitiesMap.set(groupId, []);
|
|
648
607
|
}
|
|
649
|
-
|
|
650
|
-
|
|
608
|
+
groupedEntitiesMap.get(groupId).push(entity);
|
|
609
|
+
});
|
|
610
|
+
let entityGroups = [];
|
|
611
|
+
groupedEntitiesMap.forEach((entities, groupId) => {
|
|
612
|
+
entityGroups.push({
|
|
613
|
+
label: String(groupId),
|
|
614
|
+
entities,
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
return entityGroups;
|
|
618
|
+
}
|
|
619
|
+
// private existsInCache() {
|
|
620
|
+
// const cacheKey = this.getCacheKey();
|
|
621
|
+
// if (cacheKey) {
|
|
622
|
+
// return SPMatSelectEntityComponent._entitiesCache.has(cacheKey);
|
|
623
|
+
// }
|
|
624
|
+
// return false;
|
|
625
|
+
// }
|
|
626
|
+
// private getCacheKey() {
|
|
627
|
+
// if (typeof this.url() !== 'function') {
|
|
628
|
+
// let params!: HttpParams;
|
|
629
|
+
// if (this.httpParams) {
|
|
630
|
+
// params = new HttpParams({
|
|
631
|
+
// fromString: this.httpParams.toString(),
|
|
632
|
+
// });
|
|
633
|
+
// } else {
|
|
634
|
+
// params = new HttpParams();
|
|
635
|
+
// }
|
|
636
|
+
// // params = params.set('paginate', false)
|
|
637
|
+
// return `${this.url}?${params.toString()}`;
|
|
638
|
+
// }
|
|
639
|
+
// return ''; // empty string evalutes to boolean(false)
|
|
640
|
+
// }
|
|
641
|
+
// private getFromCache() {
|
|
642
|
+
// const cacheKey = this.getCacheKey();
|
|
643
|
+
// if (cacheKey && SPMatSelectEntityComponent._entitiesCache.has(cacheKey)) {
|
|
644
|
+
// return SPMatSelectEntityComponent._entitiesCache.get(cacheKey)
|
|
645
|
+
// ?.entities as TEntity[];
|
|
646
|
+
// }
|
|
647
|
+
// return [];
|
|
648
|
+
// }
|
|
649
|
+
// private addToCache(entities: TEntity[]) {
|
|
650
|
+
// const cacheKey = this.getCacheKey();
|
|
651
|
+
// if (cacheKey) {
|
|
652
|
+
// if (!SPMatSelectEntityComponent._entitiesCache.has(cacheKey)) {
|
|
653
|
+
// SPMatSelectEntityComponent._entitiesCache.set(cacheKey, {
|
|
654
|
+
// refCount: 0,
|
|
655
|
+
// entities,
|
|
656
|
+
// });
|
|
657
|
+
// }
|
|
658
|
+
// const cacheEntry =
|
|
659
|
+
// SPMatSelectEntityComponent._entitiesCache.get(cacheKey);
|
|
660
|
+
// cacheEntry!.refCount += 1;
|
|
661
|
+
// }
|
|
662
|
+
// }
|
|
663
|
+
// private removeFromCache() {
|
|
664
|
+
// const cacheKey = this.getCacheKey();
|
|
665
|
+
// if (cacheKey) {
|
|
666
|
+
// const cacheEntry =
|
|
667
|
+
// SPMatSelectEntityComponent._entitiesCache.get(cacheKey);
|
|
668
|
+
// if (cacheEntry) {
|
|
669
|
+
// cacheEntry!.refCount -= 1;
|
|
670
|
+
// if (cacheEntry.refCount <= 0) {
|
|
671
|
+
// SPMatSelectEntityComponent._entitiesCache.delete(cacheKey);
|
|
672
|
+
// }
|
|
673
|
+
// }
|
|
674
|
+
// }
|
|
675
|
+
// }
|
|
651
676
|
getHttpReqContext() {
|
|
652
677
|
const context = new HttpContext();
|
|
653
678
|
const entityName = this.entityName;
|
|
654
679
|
context.set(SP_MAT_SELECT_ENTITY_HTTP_CONTEXT, {
|
|
655
|
-
entityName: this.entityName
|
|
656
|
-
entityNamePlural: this.
|
|
657
|
-
endpoint: this.url,
|
|
680
|
+
entityName: this.entityName(),
|
|
681
|
+
entityNamePlural: this._pluralEntityName(),
|
|
682
|
+
endpoint: this.url(),
|
|
658
683
|
});
|
|
659
684
|
return context;
|
|
660
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* If more entities are available, load the next page of entities.
|
|
688
|
+
* This method is triggered when user scrolls to the bottom of the options
|
|
689
|
+
* list. Well almost to the bottom of the options list. :)
|
|
690
|
+
*/
|
|
691
|
+
onInfiniteScroll() {
|
|
692
|
+
if (this.hasMore() && !this.loading()) {
|
|
693
|
+
this.loadNextPage();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
661
696
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: SPMatSelectEntityComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
662
|
-
/** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: SPMatSelectEntityComponent, isStandalone: true, selector: "sp-mat-select-entity", inputs: {
|
|
697
|
+
/** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: SPMatSelectEntityComponent, isStandalone: true, selector: "sp-mat-select-entity", inputs: { labelFn: { classPropertyName: "labelFn", publicName: "labelFn", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, inlineNew: { classPropertyName: "inlineNew", publicName: "inlineNew", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, groupOptionsKey: { classPropertyName: "groupOptionsKey", publicName: "groupOptionsKey", isSignal: true, isRequired: false, transformFunction: null }, groupByFn: { classPropertyName: "groupByFn", publicName: "groupByFn", isSignal: true, isRequired: false, transformFunction: null }, searchText: { classPropertyName: "searchText", publicName: "searchText", isSignal: true, isRequired: false, transformFunction: null }, notFoundText: { classPropertyName: "notFoundText", publicName: "notFoundText", isSignal: true, isRequired: false, transformFunction: null }, createNewText: { classPropertyName: "createNewText", publicName: "createNewText", isSignal: true, isRequired: false, transformFunction: null }, optionLabelTemplate: { classPropertyName: "optionLabelTemplate", publicName: "optionLabelTemplate", isSignal: true, isRequired: false, transformFunction: null }, entities: { classPropertyName: "entities", publicName: "entities", isSignal: false, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: false, isRequired: false, transformFunction: null }, userAriaDescribedBy: { classPropertyName: "userAriaDescribedBy", publicName: "aria-describedby", isSignal: false, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", createNewItemSelected: "createNewItemSelected" }, host: { properties: { "id": "this.id" } }, providers: [
|
|
663
698
|
provideTranslocoScope('sp-mat-select-entity'),
|
|
664
699
|
{ provide: MatFormFieldControl, useExisting: SPMatSelectEntityComponent },
|
|
665
|
-
], viewQueries: [{ propertyName: "
|
|
700
|
+
], viewQueries: [{ propertyName: "matSelect", first: true, predicate: MatSelect, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
|
|
666
701
|
<div
|
|
667
702
|
*transloco="let t; scope: 'sp-mat-select-entity'"
|
|
668
703
|
(focusin)="onFocusIn($event)"
|
|
@@ -674,8 +709,10 @@ class SPMatSelectEntityComponent {
|
|
|
674
709
|
[placeholder]="placeholder"
|
|
675
710
|
(opened)="onSelectOpened($event)"
|
|
676
711
|
(selectionChange)="onSelectionChange($event)"
|
|
677
|
-
[multiple]="multiple"
|
|
712
|
+
[multiple]="multiple()"
|
|
678
713
|
[(ngModel)]="selectValue"
|
|
714
|
+
msInfiniteScroll
|
|
715
|
+
(infiniteScroll)="onInfiniteScroll()"
|
|
679
716
|
>
|
|
680
717
|
<mat-select-trigger>
|
|
681
718
|
{{ selectTriggerValue }}
|
|
@@ -688,6 +725,7 @@ class SPMatSelectEntityComponent {
|
|
|
688
725
|
|
|
689
726
|
<mat-option>
|
|
690
727
|
<ngx-mat-select-search
|
|
728
|
+
class="flex-grow-1"
|
|
691
729
|
[(ngModel)]="filterStr"
|
|
692
730
|
(ngModelChange)="this.filter$.next($event)"
|
|
693
731
|
[placeholderLabel]="
|
|
@@ -701,57 +739,58 @@ class SPMatSelectEntityComponent {
|
|
|
701
739
|
</ngx-mat-select-search>
|
|
702
740
|
</mat-option>
|
|
703
741
|
|
|
704
|
-
<ng-
|
|
705
|
-
|
|
706
|
-
<ng-template #defaultOptionLabelTemplate let-entity>
|
|
707
|
-
{{ _entityLabelFn()(entity) }}
|
|
708
|
-
</ng-template>
|
|
709
|
-
@for (entity of entities; track entityId(entity)) {
|
|
710
|
-
<mat-option class="sel-entity-option" [value]="entityId(entity)">
|
|
711
|
-
<ng-container
|
|
712
|
-
*ngTemplateOutlet="
|
|
713
|
-
optionLabelTemplate() || defaultOptionLabelTemplate;
|
|
714
|
-
context: { $implicit: entity }
|
|
715
|
-
"
|
|
716
|
-
></ng-container>
|
|
717
|
-
</mat-option>
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
<!-- <mat-option class="sel-entity-option" *ngFor="let entity of entities" [value]="entityId(entity)">
|
|
721
|
-
{{ _entityLabelFn()(entity) }}
|
|
722
|
-
</mat-option> -->
|
|
723
|
-
</span>
|
|
724
|
-
</ng-container>
|
|
725
|
-
<ng-template #groupedOptions>
|
|
726
|
-
<span *ngIf="filteredGroupedValues | async as groups">
|
|
727
|
-
@for (group of groups; track groupLabel(group)) {
|
|
728
|
-
<mat-optgroup [label]="groupLabel(group)">
|
|
729
|
-
@for (entity of group.__items__; track entityId(entity)) {
|
|
730
|
-
|
|
731
|
-
<mat-option class="sel-entity-option" [value]="entityId(entity)">
|
|
732
|
-
{{ _entityLabelFn()(entity) }}
|
|
733
|
-
</mat-option>
|
|
734
|
-
}
|
|
735
|
-
</mat-optgroup>
|
|
736
|
-
}
|
|
737
|
-
</span>
|
|
742
|
+
<ng-template #defaultOptionLabelTemplate let-entity>
|
|
743
|
+
{{ _entityLabelFn()(entity) }}
|
|
738
744
|
</ng-template>
|
|
745
|
+
@if (!_group()) { @if (filteredValues | async; as entities) { @for
|
|
746
|
+
(entity of entities; track entityId(entity)) {
|
|
747
|
+
<mat-option class="sel-entity-option" [value]="entityId(entity)">
|
|
748
|
+
<ng-container
|
|
749
|
+
*ngTemplateOutlet="
|
|
750
|
+
optionLabelTemplate() || defaultOptionLabelTemplate;
|
|
751
|
+
context: { $implicit: entity }
|
|
752
|
+
"
|
|
753
|
+
></ng-container>
|
|
754
|
+
</mat-option>
|
|
755
|
+
} } } @else { @if (filteredGroupedValues | async; as groups) { @for
|
|
756
|
+
(group of groups; track group.label) {
|
|
757
|
+
<mat-optgroup [label]="group.label">
|
|
758
|
+
@for (entity of group.entities; track entityId(entity)) {
|
|
759
|
+
<mat-option class="sel-entity-option" [value]="entityId(entity)">
|
|
760
|
+
<ng-container
|
|
761
|
+
*ngTemplateOutlet="
|
|
762
|
+
optionLabelTemplate() || defaultOptionLabelTemplate;
|
|
763
|
+
context: { $implicit: entity }
|
|
764
|
+
"
|
|
765
|
+
></ng-container>
|
|
766
|
+
</mat-option>
|
|
767
|
+
}
|
|
768
|
+
</mat-optgroup>
|
|
769
|
+
} } }
|
|
739
770
|
|
|
771
|
+
<!--
|
|
772
|
+
Create New option is displayed only if there is a filter string.
|
|
773
|
+
The logic behind this behavior being that user searches for a matching
|
|
774
|
+
item and when not finding one, would like to add a new one.
|
|
775
|
+
-->
|
|
776
|
+
@if (inlineNew() && filterStr.length > 0) {
|
|
740
777
|
<mat-option
|
|
741
|
-
*ngIf="!multiple && inlineNew"
|
|
742
778
|
class="add-item-option"
|
|
743
779
|
value="0"
|
|
744
780
|
(click)="$event.stopPropagation()"
|
|
745
781
|
>⊕
|
|
746
782
|
{{
|
|
747
|
-
this.
|
|
748
|
-
? this.
|
|
749
|
-
: t('spMatSelectEntity.
|
|
750
|
-
|
|
751
|
-
|
|
783
|
+
this.createNewText()
|
|
784
|
+
? this.createNewText()
|
|
785
|
+
: t('spMatSelectEntity.createNew', {
|
|
786
|
+
item: this._capitalizedEntityName()
|
|
787
|
+
})
|
|
788
|
+
}}
|
|
789
|
+
</mat-option>
|
|
790
|
+
}
|
|
752
791
|
</mat-select>
|
|
753
792
|
</div>
|
|
754
|
-
`, isInline: true, styles: [".add-item-option{padding-top:2px;border-top:1px solid
|
|
793
|
+
`, isInline: true, styles: [".add-item-option{padding-top:2px;border-top:1px solid var(--mat-sys-outline)}.addl-selection-count{opacity:.75;font-size:.8em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "directive", type: i3.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i4.MatOptgroup, selector: "mat-optgroup", inputs: ["label", "disabled"], exportAs: ["matOptgroup"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: TranslocoModule }, { kind: "directive", type: i5.TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: NgxMatSelectSearchModule }, { kind: "component", type: i6.MatSelectSearchComponent, selector: "ngx-mat-select-search", inputs: ["placeholderLabel", "type", "closeIcon", "closeSvgIcon", "noEntriesFoundLabel", "clearSearchInput", "searching", "disableInitialFocus", "enableClearOnEscapePressed", "preventHomeEndKeyPropagation", "disableScrollToActiveOnOptionsChanged", "ariaLabel", "showToggleAllCheckbox", "toggleAllCheckboxChecked", "toggleAllCheckboxIndeterminate", "toggleAllCheckboxTooltipMessage", "toggleAllCheckboxTooltipPosition", "hideClearSearchButton", "alwaysRestoreSelectedOptionsMulti", "recreateValuesArray"], outputs: ["toggleAll"] }, { kind: "directive", type: MatSelectInfiniteScrollDirective, selector: "[msInfiniteScroll]", inputs: ["threshold", "debounceTime", "complete"], outputs: ["infiniteScroll"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
755
794
|
}
|
|
756
795
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: SPMatSelectEntityComponent, decorators: [{
|
|
757
796
|
type: Component,
|
|
@@ -767,8 +806,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
|
|
|
767
806
|
[placeholder]="placeholder"
|
|
768
807
|
(opened)="onSelectOpened($event)"
|
|
769
808
|
(selectionChange)="onSelectionChange($event)"
|
|
770
|
-
[multiple]="multiple"
|
|
809
|
+
[multiple]="multiple()"
|
|
771
810
|
[(ngModel)]="selectValue"
|
|
811
|
+
msInfiniteScroll
|
|
812
|
+
(infiniteScroll)="onInfiniteScroll()"
|
|
772
813
|
>
|
|
773
814
|
<mat-select-trigger>
|
|
774
815
|
{{ selectTriggerValue }}
|
|
@@ -781,6 +822,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
|
|
|
781
822
|
|
|
782
823
|
<mat-option>
|
|
783
824
|
<ngx-mat-select-search
|
|
825
|
+
class="flex-grow-1"
|
|
784
826
|
[(ngModel)]="filterStr"
|
|
785
827
|
(ngModelChange)="this.filter$.next($event)"
|
|
786
828
|
[placeholderLabel]="
|
|
@@ -794,54 +836,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
|
|
|
794
836
|
</ngx-mat-select-search>
|
|
795
837
|
</mat-option>
|
|
796
838
|
|
|
797
|
-
<ng-
|
|
798
|
-
|
|
799
|
-
<ng-template #defaultOptionLabelTemplate let-entity>
|
|
800
|
-
{{ _entityLabelFn()(entity) }}
|
|
801
|
-
</ng-template>
|
|
802
|
-
@for (entity of entities; track entityId(entity)) {
|
|
803
|
-
<mat-option class="sel-entity-option" [value]="entityId(entity)">
|
|
804
|
-
<ng-container
|
|
805
|
-
*ngTemplateOutlet="
|
|
806
|
-
optionLabelTemplate() || defaultOptionLabelTemplate;
|
|
807
|
-
context: { $implicit: entity }
|
|
808
|
-
"
|
|
809
|
-
></ng-container>
|
|
810
|
-
</mat-option>
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
<!-- <mat-option class="sel-entity-option" *ngFor="let entity of entities" [value]="entityId(entity)">
|
|
814
|
-
{{ _entityLabelFn()(entity) }}
|
|
815
|
-
</mat-option> -->
|
|
816
|
-
</span>
|
|
817
|
-
</ng-container>
|
|
818
|
-
<ng-template #groupedOptions>
|
|
819
|
-
<span *ngIf="filteredGroupedValues | async as groups">
|
|
820
|
-
@for (group of groups; track groupLabel(group)) {
|
|
821
|
-
<mat-optgroup [label]="groupLabel(group)">
|
|
822
|
-
@for (entity of group.__items__; track entityId(entity)) {
|
|
823
|
-
|
|
824
|
-
<mat-option class="sel-entity-option" [value]="entityId(entity)">
|
|
825
|
-
{{ _entityLabelFn()(entity) }}
|
|
826
|
-
</mat-option>
|
|
827
|
-
}
|
|
828
|
-
</mat-optgroup>
|
|
829
|
-
}
|
|
830
|
-
</span>
|
|
839
|
+
<ng-template #defaultOptionLabelTemplate let-entity>
|
|
840
|
+
{{ _entityLabelFn()(entity) }}
|
|
831
841
|
</ng-template>
|
|
842
|
+
@if (!_group()) { @if (filteredValues | async; as entities) { @for
|
|
843
|
+
(entity of entities; track entityId(entity)) {
|
|
844
|
+
<mat-option class="sel-entity-option" [value]="entityId(entity)">
|
|
845
|
+
<ng-container
|
|
846
|
+
*ngTemplateOutlet="
|
|
847
|
+
optionLabelTemplate() || defaultOptionLabelTemplate;
|
|
848
|
+
context: { $implicit: entity }
|
|
849
|
+
"
|
|
850
|
+
></ng-container>
|
|
851
|
+
</mat-option>
|
|
852
|
+
} } } @else { @if (filteredGroupedValues | async; as groups) { @for
|
|
853
|
+
(group of groups; track group.label) {
|
|
854
|
+
<mat-optgroup [label]="group.label">
|
|
855
|
+
@for (entity of group.entities; track entityId(entity)) {
|
|
856
|
+
<mat-option class="sel-entity-option" [value]="entityId(entity)">
|
|
857
|
+
<ng-container
|
|
858
|
+
*ngTemplateOutlet="
|
|
859
|
+
optionLabelTemplate() || defaultOptionLabelTemplate;
|
|
860
|
+
context: { $implicit: entity }
|
|
861
|
+
"
|
|
862
|
+
></ng-container>
|
|
863
|
+
</mat-option>
|
|
864
|
+
}
|
|
865
|
+
</mat-optgroup>
|
|
866
|
+
} } }
|
|
832
867
|
|
|
868
|
+
<!--
|
|
869
|
+
Create New option is displayed only if there is a filter string.
|
|
870
|
+
The logic behind this behavior being that user searches for a matching
|
|
871
|
+
item and when not finding one, would like to add a new one.
|
|
872
|
+
-->
|
|
873
|
+
@if (inlineNew() && filterStr.length > 0) {
|
|
833
874
|
<mat-option
|
|
834
|
-
*ngIf="!multiple && inlineNew"
|
|
835
875
|
class="add-item-option"
|
|
836
876
|
value="0"
|
|
837
877
|
(click)="$event.stopPropagation()"
|
|
838
878
|
>⊕
|
|
839
879
|
{{
|
|
840
|
-
this.
|
|
841
|
-
? this.
|
|
842
|
-
: t('spMatSelectEntity.
|
|
843
|
-
|
|
844
|
-
|
|
880
|
+
this.createNewText()
|
|
881
|
+
? this.createNewText()
|
|
882
|
+
: t('spMatSelectEntity.createNew', {
|
|
883
|
+
item: this._capitalizedEntityName()
|
|
884
|
+
})
|
|
885
|
+
}}
|
|
886
|
+
</mat-option>
|
|
887
|
+
}
|
|
845
888
|
</mat-select>
|
|
846
889
|
</div>
|
|
847
890
|
`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
@@ -850,57 +893,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
|
|
|
850
893
|
FormsModule,
|
|
851
894
|
ReactiveFormsModule,
|
|
852
895
|
MatSelectModule,
|
|
896
|
+
MatButtonModule,
|
|
897
|
+
MatIconModule,
|
|
853
898
|
TranslocoModule,
|
|
854
899
|
NgxMatSelectSearchModule,
|
|
900
|
+
MatSelectInfiniteScrollDirective,
|
|
855
901
|
], providers: [
|
|
856
902
|
provideTranslocoScope('sp-mat-select-entity'),
|
|
857
903
|
{ provide: MatFormFieldControl, useExisting: SPMatSelectEntityComponent },
|
|
858
|
-
], styles: [".add-item-option{padding-top:2px;border-top:1px solid
|
|
859
|
-
}], ctorParameters: () => [], propDecorators: {
|
|
860
|
-
type: ViewChild,
|
|
861
|
-
args: [MatSelect]
|
|
862
|
-
}], entityLabelFn: [{
|
|
863
|
-
type: Input
|
|
864
|
-
}], entityFilterFn: [{
|
|
865
|
-
type: Input,
|
|
866
|
-
args: [{ required: false }]
|
|
867
|
-
}], idKey: [{
|
|
868
|
-
type: Input,
|
|
869
|
-
args: [{ required: false }]
|
|
870
|
-
}], url: [{
|
|
871
|
-
type: Input,
|
|
872
|
-
args: [{ required: false }]
|
|
873
|
-
}], httpParams: [{
|
|
874
|
-
type: Input,
|
|
875
|
-
args: [{ required: false }]
|
|
876
|
-
}], loadFromRemoteFn: [{
|
|
877
|
-
type: Input,
|
|
878
|
-
args: [{ required: false }]
|
|
879
|
-
}], inlineNew: [{
|
|
880
|
-
type: Input,
|
|
881
|
-
args: [{ required: false }]
|
|
882
|
-
}], entityName: [{
|
|
883
|
-
type: Input,
|
|
884
|
-
args: [{ required: false }]
|
|
885
|
-
}], multiple: [{
|
|
886
|
-
type: Input,
|
|
887
|
-
args: [{ required: false }]
|
|
888
|
-
}], group: [{
|
|
889
|
-
type: Input,
|
|
890
|
-
args: [{ required: false }]
|
|
891
|
-
}], groupOptionsKey: [{
|
|
892
|
-
type: Input,
|
|
893
|
-
args: [{ required: false }]
|
|
894
|
-
}], groupLabelFn: [{
|
|
895
|
-
type: Input,
|
|
896
|
-
args: [{ required: false }]
|
|
897
|
-
}], selectionChange: [{
|
|
904
|
+
], styles: [".add-item-option{padding-top:2px;border-top:1px solid var(--mat-sys-outline)}.addl-selection-count{opacity:.75;font-size:.8em}\n"] }]
|
|
905
|
+
}], ctorParameters: () => [], propDecorators: { selectionChange: [{
|
|
898
906
|
type: Output
|
|
899
907
|
}], createNewItemSelected: [{
|
|
900
908
|
type: Output
|
|
901
|
-
}], matSelect: [{
|
|
902
|
-
type: ViewChild,
|
|
903
|
-
args: [MatSelect]
|
|
904
909
|
}], id: [{
|
|
905
910
|
type: HostBinding
|
|
906
911
|
}], entities: [{
|