@smallpearl/ngx-helper 0.33.28 → 0.33.30

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.
@@ -1,7 +1,12 @@
1
1
  import * as i1 from '@angular/common/http';
2
- import { HttpContextToken, HttpContext, HttpParams, HttpClient } from '@angular/common/http';
2
+ import { HttpContext, HttpClient, HttpParams, HttpContextToken } from '@angular/common/http';
3
3
  import * as i0 from '@angular/core';
4
- import { InjectionToken, inject, input, computed, signal, viewChild, ViewContainerRef, Component, ChangeDetectionStrategy, viewChildren, EventEmitter, effect, ContentChildren, Output, ChangeDetectorRef } from '@angular/core';
4
+ import { input, signal, computed, inject, ChangeDetectorRef, Component, InjectionToken, viewChild, ViewContainerRef, ChangeDetectionStrategy, viewChildren, EventEmitter, effect, ContentChildren, Output } from '@angular/core';
5
+ import * as i4$1 from '@jsverse/transloco';
6
+ import { TranslocoService, TranslocoModule, provideTranslocoScope } from '@jsverse/transloco';
7
+ import { setServerErrorsAsFormErrors } from '@smallpearl/ngx-helper/forms';
8
+ import { Subscription, Observable, map, tap, of, switchMap, firstValueFrom, catchError, EMPTY, throwError } from 'rxjs';
9
+ import { sideloadToComposite } from '@smallpearl/ngx-helper/sideload';
5
10
  import * as i4 from '@angular/common';
6
11
  import { CommonModule } from '@angular/common';
7
12
  import * as i2 from '@angular/material/button';
@@ -21,26 +26,12 @@ import { MatIconModule } from '@angular/material/icon';
21
26
  import * as i8 from '@angular/material/menu';
22
27
  import { MatMenuModule } from '@angular/material/menu';
23
28
  import * as i3$1 from '@angular/platform-browser';
24
- import * as i4$1 from '@jsverse/transloco';
25
- import { TranslocoService, TranslocoModule, provideTranslocoScope } from '@jsverse/transloco';
26
29
  import * as i11 from 'angular-split';
27
30
  import { AngularSplitModule } from 'angular-split';
28
31
  import { startCase, clone } from 'lodash';
29
32
  import { plural } from 'pluralize';
30
- import { Observable, of, Subscription, tap, switchMap, firstValueFrom, map, catchError, EMPTY, throwError } from 'rxjs';
31
33
  import * as i1$1 from '@angular/material/toolbar';
32
34
  import { MatToolbarModule } from '@angular/material/toolbar';
33
- import { setServerErrorsAsFormErrors } from '@smallpearl/ngx-helper/forms';
34
- import { sideloadToComposite } from '@smallpearl/ngx-helper/sideload';
35
-
36
- const SP_MAT_ENTITY_CRUD_HTTP_CONTEXT = new HttpContextToken(() => ({
37
- entityName: '',
38
- entityNamePlural: '',
39
- endpoint: '',
40
- op: undefined,
41
- }));
42
-
43
- const SP_MAT_ENTITY_CRUD_CONFIG = new InjectionToken('SPMatEntityCrudConfig');
44
35
 
45
36
  /**
46
37
  * Converts array of HttpContextToken key, value pairs to HttpContext
@@ -67,80 +58,480 @@ function convertHttpContextInputToHttpContext(context, reqContext) {
67
58
  return context;
68
59
  }
69
60
 
70
- function defaultCrudResponseParser(entityName, idKey, method, // 'create' | 'retrieve' | 'update' | 'delete',
71
- resp) {
72
- // If the response is an object with a property '<idKey>', return it as
73
- // TEntity.
74
- if (resp.hasOwnProperty(idKey)) {
75
- return resp;
76
- }
77
- // If the response has an object indexed at '<entityName>' and it has
78
- // the property '<idKey>', return it as TEntity.
79
- if (resp.hasOwnProperty(entityName)) {
80
- const obj = resp[entityName];
81
- if (obj.hasOwnProperty(idKey)) {
82
- return obj;
83
- }
84
- }
85
- // Return undefined, indicating that we could't parse the response.
86
- return undefined;
87
- }
88
- const DefaultSPMatEntityCrudConfig = {
89
- crudOpResponseParser: defaultCrudResponseParser
90
- };
91
61
  /**
92
- * To be called from an object constructor as it internally calls Angular's
93
- * inject() API.
94
- * @param userConfig
95
- * @returns
62
+ * This is a convenience base class that clients can derive from to implement
63
+ * their CRUD form component. Particularly this class registers the change
64
+ * detection hook which will be called when the user attempts to close the
65
+ * form's parent container pane via the Close button on the top right.
66
+ *
67
+ * This button behaves like a Cancel button in a desktop app and therefore if
68
+ * the user has entered any data in the form's controls, (determined by
69
+ * checking form.touched), then a 'Lose Changes' prompt is displayed allowing
70
+ * the user to cancel the closure.
71
+ *
72
+ * The `@Component` decorator is fake to keep the VSCode angular linter quiet.
73
+ *
74
+ * This class can be used in two modes:
75
+ *
76
+ * I. SPMatEntityCrudComponent mode
77
+ * This mode relies on a bridge interface that implements the
78
+ * SPMatEntityCrudCreateEditBridge interface to perform the entity
79
+ * load/create/update operations. This is the intended mode when the
80
+ * component is used as a part of the SPMatEntityCrudComponent to
81
+ * create/update an entity. This mode requires the following properties
82
+ * to be set:
83
+ * - entity: TEntity | TEntity[IdKey] | undefined (for create)
84
+ * - bridge: SPMatEntityCrudCreateEditBridge
85
+ *
86
+ * II. Standalone mode
87
+ * This mode does not rely on the bridge interface and the component
88
+ * itself performs the entity load/create/update operations.
89
+ * This mode requires the following properties to be set:
90
+ * - entity: TEntity | TEntity[IdKey] | undefined (for create)
91
+ * - baseUrl: string - Base URL for CRUD operations. This URL does not
92
+ * include the entity id. The entity id will be appended to this URL
93
+ * for entity load and update operations. For create operation, this
94
+ * URL is used as is.
95
+ * - entityName: string - Name of the entity, used to parse sideloaded
96
+ * entity responses.
97
+ * - httpReqContext?: HttpContextInput - Optional HTTP context to be
98
+ * passed to the HTTP requests. For instance, if your app has a HTTP
99
+ * interceptor that adds authentication tokens to the requests based
100
+ * on a HttpContextToken, then you can pass that token here.
101
+ *
102
+ * I. SPMatEntityCrudComponent mode:
103
+ *
104
+ * 1. Declare a FormGroup<> type as
105
+ *
106
+ * ```
107
+ * type MyForm = FormGroup<{
108
+ * name: FormControl<string>;
109
+ * type: FormControl<string>;
110
+ * notes: FormControl<string>;
111
+ * }>;
112
+ * ```
113
+ *
114
+ * 2. Derive your form's component class from this and implement the
115
+ * createForm() method returing the FormGroup<> instance that matches
116
+ * the FormGroup concrete type above.
117
+ *
118
+ * ```
119
+ * class MyFormComponent extends SPMatEntityCrudFormBase<MyForm, MyEntity> {
120
+ * constructor() {
121
+ * super()
122
+ * }
123
+ * createForm() {
124
+ * return new FormGroup([...])
125
+ * }
126
+ * }
127
+ * ```
128
+ *
129
+ * 3. If your form's value requires manipulation before being sent to the
130
+ * server, override `getFormValue()` method and do it there before returning
131
+ * the modified values.
132
+ *
133
+ * 4. Wire up the form in the template as below
134
+ *
135
+ * ```html
136
+ * @if (loadEntity$ | async) {
137
+ * <form [formGroup]='form'.. (ngSubmit)="onSubmit()">
138
+ * <button type="submit">Submit</button>
139
+ * </form>
140
+ * } @else {
141
+ * <div>Loading...</div>
142
+ * }
143
+ * ```
144
+ *
145
+ * Here `loadEntity$` is an Observable<boolean> that upon emission of `true`
146
+ * indicates that the entity has been loaded from server (in case of edit)
147
+ * and the form is ready to be displayed. Note that if the full entity was
148
+ * passed in the `entity` input property, then no server load is necessary
149
+ * and the form will be created immediately.
150
+ *
151
+ * 5. In the parent component that hosts the SPMatEntityCrudComponent, set
152
+ * the `entity` and `bridge` input properties of this component to
153
+ * appropriate values. For instance, if your form component has the
154
+ * selector `app-my-entity-form`, then the parent component's template
155
+ * will have:
156
+ *
157
+ * ```html
158
+ * <sp-mat-entity-crud
159
+ * ...
160
+ * createEditFormTemplate="entityFormTemplate"
161
+ * ></sp-mat-entity-crud>
162
+ * <ng-template #entityFormTemplate let-data="data">
163
+ * <app-my-entity-form
164
+ * [entity]="data.entity"
165
+ * [bridge]="data.bridge"
166
+ * ></app-my-entity-form>
167
+ * </ng-template>
168
+ * ```
169
+ *
170
+ * II. Standalone mode
171
+ *
172
+ * 1..4. Same as above, except set the required `bridge` input to `undefined`.
173
+ * 5. Initialize the component's inputs `baseUrl` and `entityName` with the
174
+ * appropriate values. If you would like to pass additional HTTP context to
175
+ * the HTTP requests, then set the `httpReqContext` input as well.
176
+ * If the entity uses an id key other than 'id', then set the `idKey` input
177
+ * to the appropriate id key name.
178
+ * 6. If you want to retrieve the created/updated entity after the create/update
179
+ * operation, override the `onPostCreate()` and/or `onPostUpdate()` methods
180
+ * respectively.
96
181
  */
97
- function getEntityCrudConfig() {
98
- const userCrudConfig = inject(SP_MAT_ENTITY_CRUD_CONFIG, {
99
- optional: true,
100
- });
101
- return {
102
- ...DefaultSPMatEntityCrudConfig,
103
- ...(userCrudConfig ?? {}),
104
- };
105
- }
106
-
107
- class FormViewHostComponent {
108
- entityCrudComponentBase = input.required();
109
- clientViewTemplate = input(null);
110
- _itemLabel = computed(() => {
111
- const label = this.entityCrudComponentBase().getItemLabel();
112
- return label instanceof Observable ? label : of(label);
113
- });
114
- _itemLabelPlural = computed(() => {
115
- const label = this.entityCrudComponentBase().getItemLabelPlural();
116
- return label instanceof Observable ? label : of(label);
117
- });
118
- entity = signal(undefined);
119
- title = signal(undefined);
120
- params = signal(undefined);
121
- clientFormView;
122
- vc = viewChild('clientFormContainer', { read: ViewContainerRef });
123
- config;
182
+ class SPMatEntityCrudFormBase {
183
+ // bridge mode inputs
184
+ entity = input();
185
+ bridge = input();
186
+ params = input();
187
+ // END bridge mode inputs
188
+ // standalone mode inputs
189
+ // Entity name, which is used to parse sideloaded entity responses
190
+ entityName = input();
191
+ // Base CRUD URL, which is the GET-list-of-entities/POST-to-create
192
+ // URL. Update URL will be derived from this ias `baseUrl()/${TEntity[IdKey]}`
193
+ baseUrl = input();
194
+ // Additional request context to be passed to the request
195
+ httpReqContext = input();
196
+ // ID key, defaults to 'id'
197
+ idKey = input('id');
198
+ // END standalone mode inputs
199
+ // IMPLEMENTATION
200
+ loadEntity$;
201
+ _entity = signal(undefined);
124
202
  sub$ = new Subscription();
203
+ // Store for internal form signal. form() is computed from this.
204
+ _form = signal(undefined);
205
+ // Force typecast to TFormGroup so that we can use it in the template
206
+ // without having to use the non-nullable operator ! with every reference
207
+ // of form(). In any case the form() signal is always set in ngOnInit()
208
+ // method after the form is created. And if form() is not set, then there
209
+ // will be errors while loading the form in the template.
210
+ form = computed(() => this._form());
125
211
  transloco = inject(TranslocoService);
126
- constructor() {
127
- this.config = getEntityCrudConfig();
212
+ cdr = inject(ChangeDetectorRef);
213
+ http = inject(HttpClient);
214
+ // This is really not necessary. We can check for this.bridge() directly.
215
+ mode = computed(() => {
216
+ return this.bridge() ? 'bridge' : 'standalone';
217
+ });
218
+ canCancelEdit = () => {
219
+ return this._canCancelEdit();
220
+ };
221
+ _canCancelEdit() {
222
+ const form = this._form();
223
+ if (form && form.touched) {
224
+ return window.confirm(this.transloco.translate('spMatEntityCrud.loseChangesConfirm'));
225
+ }
226
+ return true;
227
+ }
228
+ ngOnInit() {
229
+ // Validate inputs. Either bridge or (baseUrl and entityName) must be
230
+ // defined.
231
+ if (this.mode() === 'standalone' &&
232
+ (!this.getBaseUrl() || !this.getEntityName())) {
233
+ throw new Error('SPMatEntityCrudFormBase: baseUrl and entityName inputs must be defined in standalone mode.');
234
+ }
235
+ this.loadEntity$ = (typeof this.entity() === 'object' || this.entity() === undefined
236
+ ? new Observable((subscriber) => {
237
+ subscriber.next(this.entity());
238
+ subscriber.complete();
239
+ })
240
+ : this.load(this.entity())).pipe(map((resp) => {
241
+ const compositeEntity = this.getEntityFromLoadResponse(resp);
242
+ this._entity.set(compositeEntity);
243
+ this._form.set(this.createForm(compositeEntity));
244
+ const bridge = this.bridge();
245
+ if (bridge && bridge.registerCanCancelEditCallback) {
246
+ bridge.registerCanCancelEditCallback(this.canCancelEdit);
247
+ }
248
+ return true;
249
+ }));
128
250
  }
129
- ngOnInit() { }
130
251
  ngOnDestroy() {
131
252
  this.sub$.unsubscribe();
132
253
  }
133
- show(entity, params) {
134
- this.entity.set(entity);
135
- if (params && params?.title) {
136
- this.title.set(params.title instanceof Observable ? params.title : of(params.title));
137
- }
138
- else {
139
- // this.title.set(entity ? this.config.i18n.editItemLabel(this.itemLabel()) : this.config.i18n.newItemLabel(this.itemLabel()));
140
- // this.title.set(
141
- // this.transloco.translate(entity ? 'editItem' : 'newItem', {
142
- // item: this.itemLabel(),
143
- // })
254
+ /**
255
+ * Additional parameters for loading the entity, in case this.entity() value
256
+ * is of type TEntity[IdKey].
257
+ * @returns
258
+ */
259
+ getLoadEntityParams() {
260
+ return '';
261
+ }
262
+ /**
263
+ * Return the TEntity object from the response returned by the
264
+ * load() method. Typically entity load returns the actual
265
+ * entity object itself. In some cases, where response is sideloaded, the
266
+ * default implementation here uses the `sideloadToComposite()` utility to
267
+ * extract the entity from the response after merging (inplace) the
268
+ * sideloaded data into a composite.
269
+ *
270
+ * If you have a different response shape, or if your sideloaded object
271
+ * response requires custom custom `sideloadDataMap`, override this method
272
+ * and implement your custom logic to extract the TEntity object from the
273
+ * response.
274
+ * @param resp
275
+ * @returns
276
+ */
277
+ getEntityFromLoadResponse(resp) {
278
+ if (!resp || typeof resp !== 'object') {
279
+ return undefined;
280
+ }
281
+ const entityName = this.getEntityName();
282
+ if (resp.hasOwnProperty(this.getIdKey())) {
283
+ return resp;
284
+ }
285
+ else if (entityName && resp.hasOwnProperty(entityName)) {
286
+ // const sideloadDataMap = this.sideloadDataMap();
287
+ return sideloadToComposite(resp, entityName, this.getIdKey());
288
+ }
289
+ return undefined;
290
+ }
291
+ /**
292
+ * Override to customize the id key name if it's not 'id'
293
+ * @returns The name of the unique identifier key that will be used to
294
+ * extract the entity's id for UPDATE operation.
295
+ */
296
+ getIdKey() {
297
+ const bridge = this.bridge();
298
+ if (bridge) {
299
+ return bridge.getIdKey();
300
+ }
301
+ return this.idKey();
302
+ }
303
+ /**
304
+ * Return the form's value to be sent to server as Create/Update CRUD
305
+ * operation data.
306
+ * @returns
307
+ */
308
+ getFormValue() {
309
+ const form = this.form();
310
+ return form ? form.value : undefined;
311
+ }
312
+ onSubmit() {
313
+ const value = this.getFormValue();
314
+ const obs = !this._entity()
315
+ ? this.create(value)
316
+ : this.update(this._entity()[this.getIdKey()], value);
317
+ this.sub$.add(obs
318
+ ?.pipe(tap((entity) => this._entity()
319
+ ? this.onPostUpdate(entity)
320
+ : this.onPostCreate(entity)), setServerErrorsAsFormErrors(this._form(), this.cdr))
321
+ .subscribe());
322
+ }
323
+ onPostCreate(entity) {
324
+ /* empty */
325
+ }
326
+ onPostUpdate(entity) {
327
+ /* empty */
328
+ }
329
+ /**
330
+ * Loads the entity if `this.entity()` is of type TEntity[IdKey]. If `bridge`
331
+ * input is defined, then it's `loadEntity()` method is used to load the
332
+ * entity. Otherwise, then this method attempts to load the entity using
333
+ * HTTP GET from the URL derived from `baseUrl` input.
334
+ * @param entityId
335
+ * @param params
336
+ * @returns
337
+ */
338
+ load(entityId) {
339
+ const bridge = this.bridge();
340
+ const params = this.getLoadEntityParams();
341
+ if (bridge) {
342
+ return bridge.loadEntity(entityId, params);
343
+ }
344
+ // Try to load using baseUrl.
345
+ const url = this.getEntityUrl(entityId);
346
+ return this.http
347
+ .get(this.getEntityUrl(entityId), {
348
+ params: typeof params === 'string'
349
+ ? new HttpParams({ fromString: params })
350
+ : params,
351
+ context: this.getRequestContext(),
352
+ })
353
+ .pipe(map((resp) => this.getEntityFromLoadResponse(resp)));
354
+ }
355
+ /**
356
+ * Create a new entity using the bridge if defined, otherwise using HTTP
357
+ * POST to the `baseUrl`.
358
+ * @param values
359
+ * @returns
360
+ */
361
+ create(values) {
362
+ const bridge = this.bridge();
363
+ if (bridge) {
364
+ return bridge.create(values);
365
+ }
366
+ return this.http
367
+ .post(this.getBaseUrl(), values, {
368
+ context: this.getRequestContext(),
369
+ })
370
+ .pipe(map((resp) => this.getEntityFromLoadResponse(resp)));
371
+ }
372
+ /**
373
+ * Update an existing entity using the bridge if defined, otherwise using HTTP
374
+ * PATCH to the URL derived from `baseUrl` and the entity id.
375
+ * @param id
376
+ * @param values
377
+ * @returns
378
+ */
379
+ update(id, values) {
380
+ const bridge = this.bridge();
381
+ if (bridge) {
382
+ return bridge.update(id, values);
383
+ }
384
+ return this.http
385
+ .patch(this.getEntityUrl(id), values, {
386
+ context: this.getRequestContext(),
387
+ })
388
+ .pipe(map((resp) => this.getEntityFromLoadResponse(resp)));
389
+ }
390
+ /**
391
+ * Wrapper around entityName input to get the entity name. If `bridge` input
392
+ * is defined, then its `getEntityName()` method is used. This allows
393
+ * derived classes to override this method to provide custom logic to
394
+ * determine the entity name.
395
+ * @returns
396
+ */
397
+ getEntityName() {
398
+ const bridge = this.bridge();
399
+ if (bridge) {
400
+ return bridge.getEntityName();
401
+ }
402
+ return this.entityName();
403
+ }
404
+ /**
405
+ * Returns the baseUrl. Derived classes can override this to provide custom
406
+ * logic to determine the baseUrl.
407
+ * @returns
408
+ */
409
+ getBaseUrl() {
410
+ return this.baseUrl();
411
+ }
412
+ /**
413
+ * Returns the entity URL for the given entity id. If `bridge` input is
414
+ * defined, then its `getEntityUrl()` method is used. Otherwise, the URL is
415
+ * derived from `baseUrl` input.
416
+ * @param entityId
417
+ * @returns
418
+ */
419
+ getEntityUrl(entityId) {
420
+ const bridge = this.bridge();
421
+ if (bridge) {
422
+ return bridge.getEntityUrl(entityId);
423
+ }
424
+ const baseUrl = this.getBaseUrl();
425
+ if (baseUrl) {
426
+ const urlParts = baseUrl.split('?');
427
+ return `${urlParts[0]}${String(entityId)}/${urlParts[1] ? '?' + urlParts[1] : ''}`;
428
+ }
429
+ console.warn('SPMatEntityCrudFormBase.getEntityUrl: Cannot determine entity URL as neither baseUrl nor bridge inputs are provided.');
430
+ return '';
431
+ }
432
+ getRequestContext() {
433
+ let context = new HttpContext();
434
+ const httpReqContext = this.httpReqContext();
435
+ if (httpReqContext) {
436
+ context = convertHttpContextInputToHttpContext(context, httpReqContext);
437
+ }
438
+ return context;
439
+ }
440
+ /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: SPMatEntityCrudFormBase, deps: [], target: i0.ɵɵFactoryTarget.Component });
441
+ /** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.1.6", type: SPMatEntityCrudFormBase, isStandalone: false, selector: "_#_sp-mat-entity-crud-form-base_#_", inputs: { entity: { classPropertyName: "entity", publicName: "entity", isSignal: true, isRequired: false, transformFunction: null }, bridge: { classPropertyName: "bridge", publicName: "bridge", isSignal: true, isRequired: false, transformFunction: null }, params: { classPropertyName: "params", publicName: "params", isSignal: true, isRequired: false, transformFunction: null }, entityName: { classPropertyName: "entityName", publicName: "entityName", isSignal: true, isRequired: false, transformFunction: null }, baseUrl: { classPropertyName: "baseUrl", publicName: "baseUrl", isSignal: true, isRequired: false, transformFunction: null }, httpReqContext: { classPropertyName: "httpReqContext", publicName: "httpReqContext", isSignal: true, isRequired: false, transformFunction: null }, idKey: { classPropertyName: "idKey", publicName: "idKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: ``, isInline: true });
442
+ }
443
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: SPMatEntityCrudFormBase, decorators: [{
444
+ type: Component,
445
+ args: [{
446
+ selector: '_#_sp-mat-entity-crud-form-base_#_',
447
+ template: ``,
448
+ standalone: false,
449
+ }]
450
+ }] });
451
+
452
+ const SP_MAT_ENTITY_CRUD_HTTP_CONTEXT = new HttpContextToken(() => ({
453
+ entityName: '',
454
+ entityNamePlural: '',
455
+ endpoint: '',
456
+ op: undefined,
457
+ }));
458
+
459
+ const SP_MAT_ENTITY_CRUD_CONFIG = new InjectionToken('SPMatEntityCrudConfig');
460
+
461
+ function defaultCrudResponseParser(entityName, idKey, method, // 'create' | 'retrieve' | 'update' | 'delete',
462
+ resp) {
463
+ // If the response is an object with a property '<idKey>', return it as
464
+ // TEntity.
465
+ if (resp.hasOwnProperty(idKey)) {
466
+ return resp;
467
+ }
468
+ // If the response has an object indexed at '<entityName>' and it has
469
+ // the property '<idKey>', return it as TEntity.
470
+ if (resp.hasOwnProperty(entityName)) {
471
+ const obj = resp[entityName];
472
+ if (obj.hasOwnProperty(idKey)) {
473
+ return obj;
474
+ }
475
+ }
476
+ // Return undefined, indicating that we could't parse the response.
477
+ return undefined;
478
+ }
479
+ const DefaultSPMatEntityCrudConfig = {
480
+ crudOpResponseParser: defaultCrudResponseParser
481
+ };
482
+ /**
483
+ * To be called from an object constructor as it internally calls Angular's
484
+ * inject() API.
485
+ * @param userConfig
486
+ * @returns
487
+ */
488
+ function getEntityCrudConfig() {
489
+ const userCrudConfig = inject(SP_MAT_ENTITY_CRUD_CONFIG, {
490
+ optional: true,
491
+ });
492
+ return {
493
+ ...DefaultSPMatEntityCrudConfig,
494
+ ...(userCrudConfig ?? {}),
495
+ };
496
+ }
497
+
498
+ class FormViewHostComponent {
499
+ entityCrudComponentBase = input.required();
500
+ clientViewTemplate = input(null);
501
+ _itemLabel = computed(() => {
502
+ const label = this.entityCrudComponentBase().getItemLabel();
503
+ return label instanceof Observable ? label : of(label);
504
+ });
505
+ _itemLabelPlural = computed(() => {
506
+ const label = this.entityCrudComponentBase().getItemLabelPlural();
507
+ return label instanceof Observable ? label : of(label);
508
+ });
509
+ entity = signal(undefined);
510
+ title = signal(undefined);
511
+ params = signal(undefined);
512
+ clientFormView;
513
+ vc = viewChild('clientFormContainer', { read: ViewContainerRef });
514
+ config;
515
+ sub$ = new Subscription();
516
+ transloco = inject(TranslocoService);
517
+ constructor() {
518
+ this.config = getEntityCrudConfig();
519
+ }
520
+ ngOnInit() { }
521
+ ngOnDestroy() {
522
+ this.sub$.unsubscribe();
523
+ }
524
+ show(entity, params) {
525
+ this.entity.set(entity);
526
+ if (params && params?.title) {
527
+ this.title.set(params.title instanceof Observable ? params.title : of(params.title));
528
+ }
529
+ else {
530
+ // this.title.set(entity ? this.config.i18n.editItemLabel(this.itemLabel()) : this.config.i18n.newItemLabel(this.itemLabel()));
531
+ // this.title.set(
532
+ // this.transloco.translate(entity ? 'editItem' : 'newItem', {
533
+ // item: this.itemLabel(),
534
+ // })
144
535
  // );
145
536
  }
146
537
  this.params.set(params);
@@ -1713,376 +2104,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
1713
2104
  `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".preview-wrapper{display:flex;flex-direction:column;height:100%!important;width:100%!important}mat-toolbar{background-color:var(--mat-sys-surface-variant)}.spacer{flex:1 1 auto}.preview-content{padding:.4em;flex-grow:1;overflow:scroll}\n"] }]
1714
2105
  }] });
1715
2106
 
1716
- /**
1717
- * This is a convenience base class that clients can derive from to implement
1718
- * their CRUD form component. Particularly this class registers the change
1719
- * detection hook which will be called when the user attempts to close the
1720
- * form's parent container pane via the Close button on the top right.
1721
- *
1722
- * This button behaves like a Cancel button in a desktop app and therefore if
1723
- * the user has entered any data in the form's controls, (determined by
1724
- * checking form.touched), then a 'Lose Changes' prompt is displayed allowing
1725
- * the user to cancel the closure.
1726
- *
1727
- * The `@Component` decorator is fake to keep the VSCode angular linter quiet.
1728
- *
1729
- * This class can be used in two modes:
1730
- *
1731
- * I. SPMatEntityCrudComponent mode
1732
- * This mode relies on a bridge interface that implements the
1733
- * SPMatEntityCrudCreateEditBridge interface to perform the entity
1734
- * load/create/update operations. This is the intended mode when the
1735
- * component is used as a part of the SPMatEntityCrudComponent to
1736
- * create/update an entity. This mode requires the following properties
1737
- * to be set:
1738
- * - entity: TEntity | TEntity[IdKey] | undefined (for create)
1739
- * - bridge: SPMatEntityCrudCreateEditBridge
1740
- *
1741
- * II. Standalone mode
1742
- * This mode does not rely on the bridge interface and the component
1743
- * itself performs the entity load/create/update operations.
1744
- * This mode requires the following properties to be set:
1745
- * - entity: TEntity | TEntity[IdKey] | undefined (for create)
1746
- * - baseUrl: string - Base URL for CRUD operations. This URL does not
1747
- * include the entity id. The entity id will be appended to this URL
1748
- * for entity load and update operations. For create operation, this
1749
- * URL is used as is.
1750
- * - entityName: string - Name of the entity, used to parse sideloaded
1751
- * entity responses.
1752
- * - httpReqContext?: HttpContextInput - Optional HTTP context to be
1753
- * passed to the HTTP requests. For instance, if your app has a HTTP
1754
- * interceptor that adds authentication tokens to the requests based
1755
- * on a HttpContextToken, then you can pass that token here.
1756
- *
1757
- * I. SPMatEntityCrudComponent mode:
1758
- *
1759
- * 1. Declare a FormGroup<> type as
1760
- *
1761
- * ```
1762
- * type MyForm = FormGroup<{
1763
- * name: FormControl<string>;
1764
- * type: FormControl<string>;
1765
- * notes: FormControl<string>;
1766
- * }>;
1767
- * ```
1768
- *
1769
- * 2. Derive your form's component class from this and implement the
1770
- * createForm() method returing the FormGroup<> instance that matches
1771
- * the FormGroup concrete type above.
1772
- *
1773
- * ```
1774
- * class MyFormComponent extends SPMatEntityCrudFormBase<MyForm, MyEntity> {
1775
- * constructor() {
1776
- * super()
1777
- * }
1778
- * createForm() {
1779
- * return new FormGroup([...])
1780
- * }
1781
- * }
1782
- * ```
1783
- *
1784
- * 3. If your form's value requires manipulation before being sent to the
1785
- * server, override `getFormValue()` method and do it there before returning
1786
- * the modified values.
1787
- *
1788
- * 4. Wire up the form in the template as below
1789
- *
1790
- * ```html
1791
- * @if (loadEntity$ | async) {
1792
- * <form [formGroup]='form'.. (ngSubmit)="onSubmit()">
1793
- * <button type="submit">Submit</button>
1794
- * </form>
1795
- * } @else {
1796
- * <div>Loading...</div>
1797
- * }
1798
- * ```
1799
- *
1800
- * Here `loadEntity$` is an Observable<boolean> that upon emission of `true`
1801
- * indicates that the entity has been loaded from server (in case of edit)
1802
- * and the form is ready to be displayed. Note that if the full entity was
1803
- * passed in the `entity` input property, then no server load is necessary
1804
- * and the form will be created immediately.
1805
- *
1806
- * 5. In the parent component that hosts the SPMatEntityCrudComponent, set
1807
- * the `entity` and `bridge` input properties of this component to
1808
- * appropriate values. For instance, if your form component has the
1809
- * selector `app-my-entity-form`, then the parent component's template
1810
- * will have:
1811
- *
1812
- * ```html
1813
- * <sp-mat-entity-crud
1814
- * ...
1815
- * createEditFormTemplate="entityFormTemplate"
1816
- * ></sp-mat-entity-crud>
1817
- * <ng-template #entityFormTemplate let-data="data">
1818
- * <app-my-entity-form
1819
- * [entity]="data.entity"
1820
- * [bridge]="data.bridge"
1821
- * ></app-my-entity-form>
1822
- * </ng-template>
1823
- * ```
1824
- *
1825
- * II. Standalone mode
1826
- *
1827
- * 1..4. Same as above, except set the required `bridge` input to `undefined`.
1828
- * 5. Initialize the component's inputs `baseUrl` and `entityName` with the
1829
- * appropriate values. If you would like to pass additional HTTP context to
1830
- * the HTTP requests, then set the `httpReqContext` input as well.
1831
- * If the entity uses an id key other than 'id', then set the `idKey` input
1832
- * to the appropriate id key name.
1833
- * 6. If you want to retrieve the created/updated entity after the create/update
1834
- * operation, override the `onPostCreate()` and/or `onPostUpdate()` methods
1835
- * respectively.
1836
- */
1837
- class SPMatEntityCrudFormBase {
1838
- entity = input.required();
1839
- bridge = input.required();
1840
- params = input();
1841
- // --- BEGIN inputs used when `bridge` input is undefined
1842
- // Entity name, which is used to parse sideloaded entity responses
1843
- entityName = input();
1844
- // Base CRUD URL, which is the GET-list-of-entities/POST-to-create
1845
- // URL. Update URL will be derived from this ias `baseUrl()/${TEntity[IdKey]}`
1846
- baseUrl = input();
1847
- // Additional request context to be passed to the request
1848
- httpReqContext = input();
1849
- // ID key, defaults to 'id'
1850
- idKey = input('id');
1851
- // -- END inputs used when `bridge` input is undefined
1852
- // IMPLEMENTATION
1853
- loadEntity$;
1854
- _entity = signal(undefined);
1855
- sub$ = new Subscription();
1856
- // Store for internal form signal. form() is computed from this.
1857
- _form = signal(undefined);
1858
- // Force typecast to TFormGroup so that we can use it in the template
1859
- // without having to use the non-nullable operator ! with every reference
1860
- // of form(). In any case the form() signal is always set in ngOnInit()
1861
- // method after the form is created. And if form() is not set, then there
1862
- // will be errors while loading the form in the template.
1863
- form = computed(() => this._form());
1864
- transloco = inject(TranslocoService);
1865
- cdr = inject(ChangeDetectorRef);
1866
- http = inject(HttpClient);
1867
- canCancelEdit = () => {
1868
- return this._canCancelEdit();
1869
- };
1870
- _canCancelEdit() {
1871
- const form = this._form();
1872
- if (form && form.touched) {
1873
- return window.confirm(this.transloco.translate('spMatEntityCrud.loseChangesConfirm'));
1874
- }
1875
- return true;
1876
- }
1877
- ngOnInit() {
1878
- // validate inputs. Either bridge or (baseUrl and entityName) must be
1879
- // defined.
1880
- if (!this.bridge() && (!this.baseUrl() || !this.entityName())) {
1881
- throw new Error('SPMatEntityCrudFormBase: baseUrl and entityName inputs must be defined in standalone mode.');
1882
- }
1883
- this.loadEntity$ = (typeof this.entity() === 'object' || this.entity() === undefined
1884
- ? new Observable((subscriber) => {
1885
- subscriber.next(this.entity());
1886
- subscriber.complete();
1887
- })
1888
- : this.load(this.entity())).pipe(map((resp) => {
1889
- const compositeEntity = this.getEntityFromLoadResponse(resp);
1890
- this._entity.set(compositeEntity);
1891
- this._form.set(this.createForm(compositeEntity));
1892
- const bridge = this.bridge();
1893
- if (bridge && bridge.registerCanCancelEditCallback) {
1894
- bridge.registerCanCancelEditCallback(this.canCancelEdit);
1895
- }
1896
- return true;
1897
- }));
1898
- }
1899
- ngOnDestroy() {
1900
- this.sub$.unsubscribe();
1901
- }
1902
- /**
1903
- * Additional parameters for loading the entity, in case this.entity() value
1904
- * is of type TEntity[IdKey].
1905
- * @returns
1906
- */
1907
- getLoadEntityParams() {
1908
- return '';
1909
- }
1910
- /**
1911
- * Return the TEntity object from the response returned by the
1912
- * load() method. Typically entity load returns the actual
1913
- * entity object itself. In some cases, where response is sideloaded, the
1914
- * default implementation here uses the `sideloadToComposite()` utility to
1915
- * extract the entity from the response after merging (inplace) the
1916
- * sideloaded data into a composite.
1917
- *
1918
- * If you have a different response shape, or if your sideloaded object
1919
- * response requires custom custom `sideloadDataMap`, override this method
1920
- * and implement your custom logic to extract the TEntity object from the
1921
- * response.
1922
- * @param resp
1923
- * @returns
1924
- */
1925
- getEntityFromLoadResponse(resp) {
1926
- if (!resp || typeof resp !== 'object') {
1927
- return undefined;
1928
- }
1929
- const entityName = this.entityName();
1930
- if (resp.hasOwnProperty(this.getIdKey())) {
1931
- return resp;
1932
- }
1933
- else if (entityName && resp.hasOwnProperty(entityName)) {
1934
- // const sideloadDataMap = this.sideloadDataMap();
1935
- return sideloadToComposite(resp, this.entityName(), this.getIdKey());
1936
- }
1937
- return undefined;
1938
- }
1939
- /**
1940
- * Override to customize the id key name if it's not 'id'
1941
- * @returns The name of the unique identifier key that will be used to
1942
- * extract the entity's id for UPDATE operation.
1943
- */
1944
- getIdKey() {
1945
- const bridge = this.bridge();
1946
- if (bridge) {
1947
- return bridge.getIdKey();
1948
- }
1949
- return this.idKey();
1950
- }
1951
- /**
1952
- * Return the form's value to be sent to server as Create/Update CRUD
1953
- * operation data.
1954
- * @returns
1955
- */
1956
- getFormValue() {
1957
- const form = this.form();
1958
- return form ? form.value : undefined;
1959
- }
1960
- onSubmit() {
1961
- const value = this.getFormValue();
1962
- const obs = !this._entity()
1963
- ? this.create(value)
1964
- : this.update(this._entity()[this.getIdKey()], value);
1965
- this.sub$.add(obs
1966
- ?.pipe(tap(entity => this._entity() ? this.onPostUpdate(entity) : this.onPostCreate(entity)), setServerErrorsAsFormErrors(this._form(), this.cdr))
1967
- .subscribe());
1968
- }
1969
- onPostCreate(entity) {
1970
- /* empty */
1971
- }
1972
- onPostUpdate(entity) {
1973
- /* empty */
1974
- }
1975
- /**
1976
- * Loads the entity if `this.entity()` is of type TEntity[IdKey]. If `bridge`
1977
- * input is defined, then it's `loadEntity()` method is used to load the
1978
- * entity. Otherwise, then this method attempts to load the entity using
1979
- * HTTP GET from the URL derived from `baseUrl` input.
1980
- * @param entityId
1981
- * @param params
1982
- * @returns
1983
- */
1984
- load(entityId) {
1985
- const bridge = this.bridge();
1986
- const params = this.getLoadEntityParams();
1987
- if (bridge) {
1988
- return bridge.loadEntity(entityId, params);
1989
- }
1990
- // Try to load using baseUrl.
1991
- if (!this.baseUrl()) {
1992
- console.warn(`SPMatEntityCrudFormBase.load: No bridge defined, baseUrl input is undefined. Returning undefined.`);
1993
- return new Observable((subscriber) => {
1994
- subscriber.next(undefined);
1995
- subscriber.complete();
1996
- });
1997
- }
1998
- let context = new HttpContext();
1999
- if (this.httpReqContext()) {
2000
- context = convertHttpContextInputToHttpContext(context, this.httpReqContext());
2001
- }
2002
- const url = this.getEntityUrl(entityId);
2003
- return this.http
2004
- .get(this.getEntityUrl(entityId), {
2005
- params: typeof params === 'string'
2006
- ? new HttpParams({ fromString: params })
2007
- : params,
2008
- context: context,
2009
- })
2010
- .pipe(map((resp) => this.getEntityFromLoadResponse(resp)));
2011
- }
2012
- /**
2013
- * Create a new entity using the bridge if defined, otherwise using HTTP
2014
- * POST to the `baseUrl`.
2015
- * @param values
2016
- * @returns
2017
- */
2018
- create(values) {
2019
- const bridge = this.bridge();
2020
- if (bridge) {
2021
- return bridge.create(values);
2022
- }
2023
- const url = this.baseUrl();
2024
- if (!url) {
2025
- console.warn('SPMatEntityCrudFormBase.create: Cannot create entity as neither bridge nor baseUrl inputs are provided.');
2026
- return of(undefined);
2027
- }
2028
- const httpReqContext = this.httpReqContext();
2029
- let context = new HttpContext();
2030
- if (httpReqContext) {
2031
- context = convertHttpContextInputToHttpContext(context, httpReqContext);
2032
- }
2033
- return this.http
2034
- .post(url, values, { context: context })
2035
- .pipe(map((resp) => this.getEntityFromLoadResponse(resp)));
2036
- }
2037
- /**
2038
- * Update an existing entity using the bridge if defined, otherwise using HTTP
2039
- * PATCH to the URL derived from `baseUrl` and the entity id.
2040
- * @param id
2041
- * @param values
2042
- * @returns
2043
- */
2044
- update(id, values) {
2045
- const bridge = this.bridge();
2046
- if (bridge) {
2047
- return bridge.update(id, values);
2048
- }
2049
- const url = this.baseUrl();
2050
- if (!url) {
2051
- console.warn('SPMatEntityCrudFormBase.update: Cannot update entity as neither bridge nor baseUrl inputs are provided.');
2052
- return of(undefined);
2053
- }
2054
- return this.http
2055
- .patch(this.getEntityUrl(id), values)
2056
- .pipe(map((resp) => this.getEntityFromLoadResponse(resp)));
2057
- }
2058
- getEntityUrl(entityId) {
2059
- const bridge = this.bridge();
2060
- if (bridge) {
2061
- return bridge.getEntityUrl(entityId);
2062
- }
2063
- const baseUrl = this.baseUrl();
2064
- if (baseUrl) {
2065
- const urlParts = baseUrl.split('?');
2066
- return `${urlParts[0]}${String(entityId)}/${urlParts[1] ? '?' + urlParts[1] : ''}`;
2067
- }
2068
- console.warn('SPMatEntityCrudFormBase.getEntityUrl: Cannot determine entity URL as neither baseUrl nor bridge inputs are provided.');
2069
- return '';
2070
- }
2071
- /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: SPMatEntityCrudFormBase, deps: [], target: i0.ɵɵFactoryTarget.Component });
2072
- /** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.1.6", type: SPMatEntityCrudFormBase, isStandalone: false, selector: "_#_sp-mat-entity-crud-form-base_#_", inputs: { entity: { classPropertyName: "entity", publicName: "entity", isSignal: true, isRequired: true, transformFunction: null }, bridge: { classPropertyName: "bridge", publicName: "bridge", isSignal: true, isRequired: true, transformFunction: null }, params: { classPropertyName: "params", publicName: "params", isSignal: true, isRequired: false, transformFunction: null }, entityName: { classPropertyName: "entityName", publicName: "entityName", isSignal: true, isRequired: false, transformFunction: null }, baseUrl: { classPropertyName: "baseUrl", publicName: "baseUrl", isSignal: true, isRequired: false, transformFunction: null }, httpReqContext: { classPropertyName: "httpReqContext", publicName: "httpReqContext", isSignal: true, isRequired: false, transformFunction: null }, idKey: { classPropertyName: "idKey", publicName: "idKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: ``, isInline: true });
2073
- }
2074
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: SPMatEntityCrudFormBase, decorators: [{
2075
- type: Component,
2076
- args: [{
2077
- selector: '_#_sp-mat-entity-crud-form-base_#_',
2078
- template: ``,
2079
- standalone: false,
2080
- }]
2081
- }] });
2082
-
2083
2107
  /**
2084
2108
  * Generated bundle index. Do not edit.
2085
2109
  */
2086
2110
 
2087
- export { SPMatEntityCrudComponent, SPMatEntityCrudFormBase, SPMatEntityCrudPreviewPaneComponent, SP_MAT_ENTITY_CRUD_CONFIG, SP_MAT_ENTITY_CRUD_HTTP_CONTEXT };
2111
+ export { SPMatEntityCrudComponent, SPMatEntityCrudFormBase, SPMatEntityCrudPreviewPaneComponent, SP_MAT_ENTITY_CRUD_CONFIG, SP_MAT_ENTITY_CRUD_HTTP_CONTEXT, convertHttpContextInputToHttpContext };
2088
2112
  //# sourceMappingURL=smallpearl-ngx-helper-mat-entity-crud.mjs.map