From c39a69df3e04c1cce01587aba182e176951735f9 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 11 Jul 2022 21:34:19 +0300 Subject: [PATCH] remove some constructor dependencies --- src/lib/dialog/base-dialog.component.ts | 23 +++--- src/lib/listing/index.ts | 1 + .../listing/listing-component.directive.ts | 33 ++++----- src/lib/listing/services/entities.service.ts | 42 +++++------ src/lib/listing/utils.ts | 70 +++++++++++++++++++ src/lib/services/entities-map.service.ts | 20 +++--- src/lib/services/generic.service.ts | 28 ++++---- src/lib/services/stats.service.ts | 23 +++--- src/lib/utils/functions.ts | 10 +++ tsconfig.json | 2 +- 10 files changed, 158 insertions(+), 94 deletions(-) create mode 100644 src/lib/listing/utils.ts diff --git a/src/lib/dialog/base-dialog.component.ts b/src/lib/dialog/base-dialog.component.ts index 341ffdf..b703d12 100644 --- a/src/lib/dialog/base-dialog.component.ts +++ b/src/lib/dialog/base-dialog.component.ts @@ -1,10 +1,12 @@ -import { Directive, HostListener, Injector, OnInit } from '@angular/core'; +import { Directive, HostListener, inject, OnInit } from '@angular/core'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { UntypedFormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { AutoUnsubscribe, hasFormChanged, IqserEventTarget } from '../utils'; import { ConfirmOptions } from '.'; import { ConfirmationDialogService } from './confirmation-dialog.service'; import { firstValueFrom } from 'rxjs'; +import { LoadingService } from '../loading'; +import { Toaster } from '../services'; const TARGET_NODE = 'mat-dialog-container'; @@ -27,14 +29,13 @@ export interface SaveOptions { export abstract class BaseDialogComponent extends AutoUnsubscribe implements OnInit { form!: UntypedFormGroup; initialFormValue!: Record; - private readonly _confirmationDialogService: ConfirmationDialogService = this._injector.get(ConfirmationDialogService); - private readonly _dialog: MatDialog = this._injector.get(MatDialog); + protected readonly _formBuilder = inject(UntypedFormBuilder); + protected readonly _loadingService = inject(LoadingService); + protected readonly _toaster = inject(Toaster); + readonly #confirmationDialogService = inject(ConfirmationDialogService); + readonly #dialog = inject(MatDialog); - constructor( - protected readonly _injector: Injector, - protected readonly _dialogRef: MatDialogRef, - private readonly _isInEditMode?: boolean, - ) { + constructor(protected readonly _dialogRef: MatDialogRef, private readonly _isInEditMode?: boolean) { super(); } @@ -85,13 +86,13 @@ export abstract class BaseDialogComponent extends AutoUnsubscribe implements OnI @HostListener('window:keydown.Escape', ['$event']) onEscape(): void { - if (this._dialog.openDialogs.length === 1) { + if (this.#dialog.openDialogs.length === 1) { this.close(); } } protected _openConfirmDialog() { - const dialogRef = this._confirmationDialogService.openDialog({ disableConfirm: !this.valid }); + const dialogRef = this.#confirmationDialogService.openDialog({ disableConfirm: !this.valid }); return firstValueFrom(dialogRef.afterClosed()); } } diff --git a/src/lib/listing/index.ts b/src/lib/listing/index.ts index 208abd9..3eae889 100644 --- a/src/lib/listing/index.ts +++ b/src/lib/listing/index.ts @@ -1,4 +1,5 @@ export * from './models'; +export * from './utils'; export * from './services'; export * from './scroll-button/scroll-button.component'; diff --git a/src/lib/listing/listing-component.directive.ts b/src/lib/listing/listing-component.directive.ts index 45e9da3..c7c700a 100644 --- a/src/lib/listing/listing-component.directive.ts +++ b/src/lib/listing/listing-component.directive.ts @@ -1,4 +1,4 @@ -import { Directive, Injector, OnDestroy, TemplateRef, ViewChild } from '@angular/core'; +import { Directive, inject, OnDestroy, TemplateRef, ViewChild } from '@angular/core'; import { combineLatest, Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { FilterService } from '../filtering'; @@ -8,20 +8,17 @@ import { SearchService } from '../search'; import { EntitiesService, ListingService } from './services'; import { IListable, TableColumnConfig } from './models'; -export const DefaultListingServicesTmp = [FilterService, SearchService, SortingService, ListingService] as const; -export const DefaultListingServices = [...DefaultListingServicesTmp, EntitiesService] as const; - @Directive() export abstract class ListingComponent extends AutoUnsubscribe implements OnDestroy { - readonly filterService = this._injector.get(FilterService); - readonly searchService = this._injector.get>(SearchService); - readonly sortingService = this._injector.get>(SortingService); - readonly entitiesService = this._injector.get>(EntitiesService); - readonly listingService = this._injector.get>(ListingService); + readonly filterService = inject(FilterService); + readonly searchService = inject>(SearchService); + readonly sortingService = inject>(SortingService); + readonly entitiesService = inject>(EntitiesService); + readonly listingService = inject>(ListingService); - readonly noMatch$ = this._noMatch$; - readonly noContent$ = this._noContent$; - readonly sortedDisplayedEntities$ = this._sortedDisplayedEntities$; + readonly noMatch$ = this.#noMatch$; + readonly noContent$ = this.#noContent$; + readonly sortedDisplayedEntities$ = this.#sortedDisplayedEntities$; abstract readonly tableColumnConfigs: readonly TableColumnConfig[]; abstract readonly tableHeaderLabel: string; @@ -29,15 +26,11 @@ export abstract class ListingComponent extends AutoUnsubscr @ViewChild('tableItemTemplate') readonly tableItemTemplate?: TemplateRef; @ViewChild('workflowItemTemplate') readonly workflowItemTemplate?: TemplateRef; - protected constructor(protected readonly _injector: Injector) { - super(); - } - get allEntities(): T[] { return this.entitiesService.all; } - private get _sortedDisplayedEntities$(): Observable { + get #sortedDisplayedEntities$(): Observable { const sort = (entities: T[]) => this.sortingService.defaultSort(entities); const sortedEntities$ = this.listingService.displayed$.pipe(map(sort)); return this.sortingService.sortingOption$.pipe( @@ -46,15 +39,15 @@ export abstract class ListingComponent extends AutoUnsubscr ); } - private get _noMatch$(): Observable { + get #noMatch$(): Observable { return combineLatest([this.entitiesService.allLength$, this.listingService.displayedLength$]).pipe( map(([hasEntities, hasDisplayedEntities]) => !!hasEntities && !hasDisplayedEntities), shareDistinctLast(), ); } - private get _noContent$(): Observable { - return combineLatest([this._noMatch$, this.entitiesService.noData$]).pipe( + get #noContent$(): Observable { + return combineLatest([this.#noMatch$, this.entitiesService.noData$]).pipe( map(([noMatch, noData]) => noMatch || noData), shareDistinctLast(), ); diff --git a/src/lib/listing/services/entities.service.ts b/src/lib/listing/services/entities.service.ts index 06a590a..218d49d 100644 --- a/src/lib/listing/services/entities.service.ts +++ b/src/lib/listing/services/entities.service.ts @@ -1,46 +1,38 @@ -import { Inject, Injectable, InjectionToken, Injector, Optional } from '@angular/core'; +import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { filter, map, startWith, tap } from 'rxjs/operators'; import { IListable } from '../models'; import { GenericService, QueryParam } from '../../services'; import { getLength, List, mapEach, shareDistinctLast, shareLast } from '../../utils'; -/** - * This should be removed when refactoring is done - */ -const ENTITY_PATH = new InjectionToken('This is here for compatibility while refactoring things.'); -const ENTITY_CLASS = new InjectionToken('This is here for compatibility while refactoring things.'); - @Injectable() /** - * E for Entity - * I for Interface + * E for Entity, + * I for Interface. * By default, if no interface is provided, I = E */ -export class EntitiesService extends GenericService { +export class EntitiesService extends GenericService { readonly noData$: Observable; readonly all$: Observable; readonly allLength$: Observable; + protected readonly _defaultModelPath: string = ''; + protected readonly _entityClass?: new (entityInterface: I, ...args: unknown[]) => E; protected readonly _entityChanged$ = new Subject(); protected readonly _entityDeleted$ = new Subject(); - private readonly _all$ = new BehaviorSubject([]); + readonly #all$ = new BehaviorSubject([]); - constructor( - protected readonly _injector: Injector, - @Optional() @Inject(ENTITY_CLASS) private readonly _entityClass: new (entityInterface: I, ...args: unknown[]) => E, - @Optional() @Inject(ENTITY_PATH) protected readonly _defaultModelPath = '', - ) { - super(_injector, _defaultModelPath); - this.all$ = this._all$.asObservable().pipe(shareDistinctLast()); - this.allLength$ = this._all$.pipe(getLength, shareDistinctLast()); - this.noData$ = this._noData$; + constructor() { + super(); + this.all$ = this.#all$.asObservable().pipe(shareDistinctLast()); + this.allLength$ = this.#all$.pipe(getLength, shareDistinctLast()); + this.noData$ = this.#noData$; } get all(): E[] { - return Object.values(this._all$.getValue()); + return Object.values(this.#all$.getValue()); } - private get _noData$(): Observable { + get #noData$(): Observable { return this.allLength$.pipe( map(length => length === 0), shareDistinctLast(), @@ -50,7 +42,7 @@ export class EntitiesService extends GenericService< loadAll(...args: unknown[]): Observable; loadAll(modelPath = this._defaultModelPath, queryParams?: List): Observable { return this.getAll(modelPath, queryParams).pipe( - mapEach(entity => new this._entityClass(entity)), + mapEach(entity => (this._entityClass ? new this._entityClass(entity) : (entity as E))), tap((entities: E[]) => this.setEntities(entities)), ); } @@ -83,7 +75,7 @@ export class EntitiesService extends GenericService< return entity; }); - this._all$.next(newEntities); + this.#all$.next(newEntities); // Emit observables only after entities have been updated @@ -99,7 +91,7 @@ export class EntitiesService extends GenericService< remove(id: string) { const entity = this.all.find(item => item.id === id); if (entity) { - this._all$.next(this.all.filter(item => item.id !== id)); + this.#all$.next(this.all.filter(item => item.id !== id)); this._entityDeleted$.next(entity); } } diff --git a/src/lib/listing/utils.ts b/src/lib/listing/utils.ts new file mode 100644 index 0000000..40f7010 --- /dev/null +++ b/src/lib/listing/utils.ts @@ -0,0 +1,70 @@ +import { FilterService } from '../filtering'; +import { SearchService } from '../search'; +import { SortingService } from '../sorting'; +import { EntitiesService, ListingService } from './services'; +import { forwardRef, Provider, Type } from '@angular/core'; +import { ListingComponent } from './listing-component.directive'; + +export const DefaultListingServices: readonly Provider[] = [FilterService, SearchService, SortingService, ListingService] as const; + +export interface IListingServiceFactoryOptions { + readonly entitiesService?: Type>; + readonly component: Type; +} + +function getEntitiesService(service?: Type>): Provider { + if (service) { + return { + provide: EntitiesService, + useExisting: service, + }; + } + + return EntitiesService; +} + +function getOptions(options?: IListingServiceFactoryOptions | Type) { + if (typeof options === 'function') { + return { + component: options, + }; + } + + return options; +} + +function getComponent(component: Type) { + return { + provide: ListingComponent, + useExisting: forwardRef(() => component), + }; +} + +/** + * This is equivalent to + * @example [FilterService, SearchService, SortingService, ListingService, EntitiesService] + */ +export function listingProvidersFactory(): Provider[]; +/** + * This is equivalent to + * @example [{provide: ListingComponent, useExisting: forwardRef(() => component)}, EntitiesService, FilterService, SearchService, SortingService, ListingService] + */ +export function listingProvidersFactory(component: Type): Provider[]; +/** + * This is equivalent to + * @example [{provide: EntitiesService, useExisting: entitiesService}, {provide: ListingComponent, useExisting: forwardRef(() => component)}, FilterService, SearchService, SortingService, ListingService] + */ +export function listingProvidersFactory(options: IListingServiceFactoryOptions): Provider[]; +export function listingProvidersFactory(args?: IListingServiceFactoryOptions | Type): Provider[] { + const options = getOptions(args); + const services = [...DefaultListingServices]; + + const entitiesService = getEntitiesService(options?.entitiesService); + services.push(entitiesService); + + if (options?.component) { + services.push(getComponent(options.component)); + } + + return services; +} diff --git a/src/lib/services/entities-map.service.ts b/src/lib/services/entities-map.service.ts index 59e4e91..9db2413 100644 --- a/src/lib/services/entities-map.service.ts +++ b/src/lib/services/entities-map.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { filter, startWith } from 'rxjs/operators'; import { Entity } from '../listing'; @@ -6,11 +6,11 @@ import { RequiredParam, shareLast, Validate } from '../utils'; @Injectable({ providedIn: 'root' }) export abstract class EntitiesMapService, I> { - protected readonly _map = new Map>(); - private readonly _entityChanged$ = new Subject(); - private readonly _entityDeleted$ = new Subject(); + protected abstract readonly _primaryKey: string; - protected constructor(@Inject('ENTITY_PRIMARY_KEY') protected readonly _primaryKey: string) {} + protected readonly _map = new Map>(); + readonly #entityChanged$ = new Subject(); + readonly #entityDeleted$ = new Subject(); get empty(): boolean { return this._map.size === 0; @@ -45,7 +45,7 @@ export abstract class EntitiesMapService, I> { set(key: string, entities: E[]): void { if (!this._map.has(key)) { this._map.set(key, new BehaviorSubject(entities)); - return entities.forEach(entity => this._entityChanged$.next(entity)); + return entities.forEach(entity => this.#entityChanged$.next(entity)); } const changedEntities: E[] = []; @@ -68,11 +68,11 @@ export abstract class EntitiesMapService, I> { // Emit observables only after entities have been updated for (const entity of changedEntities) { - this._entityChanged$.next(entity); + this.#entityChanged$.next(entity); } for (const entity of deletedEntities) { - this._entityDeleted$.next(entity); + this.#entityDeleted$.next(entity); } } @@ -97,7 +97,7 @@ export abstract class EntitiesMapService, I> { @Validate() watch$(@RequiredParam() key: string, @RequiredParam() entityId: string): Observable { - return this._entityChanged$.pipe( + return this.#entityChanged$.pipe( filter(entity => entity.id === entityId), startWith(this.get(key, entityId) as E), shareLast(), @@ -105,7 +105,7 @@ export abstract class EntitiesMapService, I> { } watchDeleted$(entityId: string): Observable { - return this._entityDeleted$.pipe(filter(entity => entity.id === entityId)); + return this.#entityDeleted$.pipe(filter(entity => entity.id === entityId)); } private _pluckPrimaryKey(entity: E): string { diff --git a/src/lib/services/generic.service.ts b/src/lib/services/generic.service.ts index f3e0950..ed46356 100644 --- a/src/lib/services/generic.service.ts +++ b/src/lib/services/generic.service.ts @@ -1,5 +1,5 @@ import { HttpClient, HttpEvent, HttpParams } from '@angular/common/http'; -import { Injector } from '@angular/core'; +import { inject } from '@angular/core'; import { Observable } from 'rxjs'; import { CustomHttpUrlEncodingCodec, HeadersConfiguration, List, RequiredParam, Validate } from '../utils'; import { map, tap } from 'rxjs/operators'; @@ -18,31 +18,31 @@ export interface QueryParam { } /** - * I for interface + * I for interface, * R for response */ export abstract class GenericService { - protected readonly _http = this._injector.get(HttpClient); + protected readonly _http = inject(HttpClient); protected readonly _lastCheckedForChanges = new Map([[ROOT_CHANGES_KEY, '0']]); - - protected constructor(protected readonly _injector: Injector, protected readonly _defaultModelPath: string) {} + protected abstract readonly _defaultModelPath: string; get(): Observable; - // eslint-disable-next-line @typescript-eslint/unified-signatures + get(): Observable; get(id: string, ...args: unknown[]): Observable; - // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars + get(id: string, ...args: unknown[]): Observable; get(id?: string, ...args: unknown[]): Observable { return id ? this._getOne([id]) : this.getAll(); } + getAll(modelPath?: string, queryParams?: List): Observable; + getAll(modelPath?: string, queryParams?: List): Observable; getAll(modelPath = this._defaultModelPath, queryParams?: List): Observable { - return this._http - .get(`/${encodeURI(modelPath)}`, { - headers: HeadersConfiguration.getHeaders({ contentType: false }), - observe: 'body', - params: this._queryParams(queryParams), - }) - .pipe(tap(() => this._updateLastChanged())); + const request$ = this._http.get(`/${encodeURI(modelPath)}`, { + headers: HeadersConfiguration.getHeaders({ contentType: false }), + observe: 'body', + params: this._queryParams(queryParams), + }); + return request$.pipe(tap(() => this._updateLastChanged())); } getFor(entityId: string, queryParams?: List): Observable { diff --git a/src/lib/services/stats.service.ts b/src/lib/services/stats.service.ts index 3be767c..ff88be8 100644 --- a/src/lib/services/stats.service.ts +++ b/src/lib/services/stats.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Injector } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { tap } from 'rxjs/operators'; @@ -6,19 +6,16 @@ import { HeadersConfiguration, mapEach, RequiredParam, Validate } from '../utils @Injectable() export abstract class StatsService { - private readonly _http = this._injector.get(HttpClient); - private readonly _map = new Map>(); + protected abstract readonly _primaryKey: string; + protected abstract readonly _entityClass: new (entityInterface: I, ...args: unknown[]) => E; + protected abstract readonly _defaultModelPath: string; - protected constructor( - protected readonly _injector: Injector, - @Inject('ENTITY_PRIMARY_KEY') protected readonly _primaryKey: string, - @Inject('ENTITY_CLASS') private readonly _entityClass: new (entityInterface: I, ...args: unknown[]) => E, - @Inject('ENTITY_PATH') protected readonly _defaultModelPath: string, - ) {} + readonly #http = inject(HttpClient); + readonly #map = new Map>(); @Validate() getFor(@RequiredParam() ids: string[]): Observable { - const request = this._http.post(`/${encodeURI(this._defaultModelPath)}`, ids, { + const request = this.#http.post(`/${encodeURI(this._defaultModelPath)}`, ids, { headers: HeadersConfiguration.getHeaders(), observe: 'body', }); @@ -34,8 +31,8 @@ export abstract class StatsService { } set(stats: E): void { - if (!this._map.has(this._pluckPrimaryKey(stats))) { - this._map.set(this._pluckPrimaryKey(stats), new BehaviorSubject(stats)); + if (!this.#map.has(this._pluckPrimaryKey(stats))) { + this.#map.set(this._pluckPrimaryKey(stats), new BehaviorSubject(stats)); return; } @@ -57,6 +54,6 @@ export abstract class StatsService { private _getBehaviourSubject(key: string): BehaviorSubject { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this._map.get(key)!; + return this.#map.get(key)!; } } diff --git a/src/lib/utils/functions.ts b/src/lib/utils/functions.ts index a208c33..3fa8a1a 100644 --- a/src/lib/utils/functions.ts +++ b/src/lib/utils/functions.ts @@ -2,6 +2,8 @@ import { ITrackable } from '../listing/models/trackable'; import { UntypedFormGroup } from '@angular/forms'; import { forOwn, has, isEqual, isPlainObject, transform } from 'lodash-es'; import dayjs, { Dayjs } from 'dayjs'; +import { inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; export function capitalize(value: string | String): string { if (!value) { @@ -165,3 +167,11 @@ String.prototype.capitalize = function _capitalize(this: string): string { Array.prototype.filterTruthy = function (this: T[], predicate: (value: T) => boolean = () => true): T[] { return this.filter(value => !!value && predicate(value)); }; + +/** + * Use this in field initialization or in constructor of a service / component + * @param param + */ +export function getParam(param: string): string | null { + return inject(ActivatedRoute).snapshot.paramMap.get(param); +} diff --git a/tsconfig.json b/tsconfig.json index f820412..d76b45e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "lib": ["es2021", "dom"], "allowSyntheticDefaultImports": true }, - "include": ["**/*.ts"], + "include": ["src/**/*.ts"], "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true,