remove some constructor dependencies

This commit is contained in:
Dan Percic 2022-07-11 21:34:19 +03:00
parent 10de0152c7
commit c39a69df3e
10 changed files with 158 additions and 94 deletions

View File

@ -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<string, string>;
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<BaseDialogComponent>,
private readonly _isInEditMode?: boolean,
) {
constructor(protected readonly _dialogRef: MatDialogRef<BaseDialogComponent>, 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());
}
}

View File

@ -1,4 +1,5 @@
export * from './models';
export * from './utils';
export * from './services';
export * from './scroll-button/scroll-button.component';

View File

@ -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<T extends IListable> extends AutoUnsubscribe implements OnDestroy {
readonly filterService = this._injector.get(FilterService);
readonly searchService = this._injector.get<SearchService<T>>(SearchService);
readonly sortingService = this._injector.get<SortingService<T>>(SortingService);
readonly entitiesService = this._injector.get<EntitiesService<T>>(EntitiesService);
readonly listingService = this._injector.get<ListingService<T>>(ListingService);
readonly filterService = inject(FilterService);
readonly searchService = inject<SearchService<T>>(SearchService);
readonly sortingService = inject<SortingService<T>>(SortingService);
readonly entitiesService = inject<EntitiesService<T>>(EntitiesService);
readonly listingService = inject<ListingService<T>>(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<T>[];
abstract readonly tableHeaderLabel: string;
@ -29,15 +26,11 @@ export abstract class ListingComponent<T extends IListable> extends AutoUnsubscr
@ViewChild('tableItemTemplate') readonly tableItemTemplate?: TemplateRef<unknown>;
@ViewChild('workflowItemTemplate') readonly workflowItemTemplate?: TemplateRef<unknown>;
protected constructor(protected readonly _injector: Injector) {
super();
}
get allEntities(): T[] {
return this.entitiesService.all;
}
private get _sortedDisplayedEntities$(): Observable<T[]> {
get #sortedDisplayedEntities$(): Observable<T[]> {
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<T extends IListable> extends AutoUnsubscr
);
}
private get _noMatch$(): Observable<boolean> {
get #noMatch$(): Observable<boolean> {
return combineLatest([this.entitiesService.allLength$, this.listingService.displayedLength$]).pipe(
map(([hasEntities, hasDisplayedEntities]) => !!hasEntities && !hasDisplayedEntities),
shareDistinctLast(),
);
}
private get _noContent$(): Observable<boolean> {
return combineLatest([this._noMatch$, this.entitiesService.noData$]).pipe(
get #noContent$(): Observable<boolean> {
return combineLatest([this.#noMatch$, this.entitiesService.noData$]).pipe(
map(([noMatch, noData]) => noMatch || noData),
shareDistinctLast(),
);

View File

@ -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<string>('This is here for compatibility while refactoring things.');
const ENTITY_CLASS = new InjectionToken<string>('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<E extends IListable, I = E> extends GenericService<I> {
export class EntitiesService<I, E extends I & IListable = I & IListable> extends GenericService<I> {
readonly noData$: Observable<boolean>;
readonly all$: Observable<E[]>;
readonly allLength$: Observable<number>;
protected readonly _defaultModelPath: string = '';
protected readonly _entityClass?: new (entityInterface: I, ...args: unknown[]) => E;
protected readonly _entityChanged$ = new Subject<E>();
protected readonly _entityDeleted$ = new Subject<E>();
private readonly _all$ = new BehaviorSubject<E[]>([]);
readonly #all$ = new BehaviorSubject<E[]>([]);
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<boolean> {
get #noData$(): Observable<boolean> {
return this.allLength$.pipe(
map(length => length === 0),
shareDistinctLast(),
@ -50,7 +42,7 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
loadAll(...args: unknown[]): Observable<E[]>;
loadAll(modelPath = this._defaultModelPath, queryParams?: List<QueryParam>): Observable<E[]> {
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<E extends IListable, I = E> 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<E extends IListable, I = E> 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);
}
}

70
src/lib/listing/utils.ts Normal file
View File

@ -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<T> {
readonly entitiesService?: Type<EntitiesService<T>>;
readonly component: Type<unknown>;
}
function getEntitiesService<T>(service?: Type<EntitiesService<T>>): Provider {
if (service) {
return {
provide: EntitiesService,
useExisting: service,
};
}
return EntitiesService;
}
function getOptions<T>(options?: IListingServiceFactoryOptions<T> | Type<unknown>) {
if (typeof options === 'function') {
return {
component: options,
};
}
return options;
}
function getComponent<T>(component: Type<unknown>) {
return {
provide: ListingComponent,
useExisting: forwardRef(() => component),
};
}
/**
* This is equivalent to
* @example <code>[FilterService, SearchService, SortingService, ListingService, EntitiesService]</code>
*/
export function listingProvidersFactory(): Provider[];
/**
* This is equivalent to
* @example <code>[{provide: ListingComponent, useExisting: forwardRef(() => component)}, EntitiesService, FilterService, SearchService, SortingService, ListingService]</code>
*/
export function listingProvidersFactory(component: Type<unknown>): Provider[];
/**
* This is equivalent to
* @example <code>[{provide: EntitiesService, useExisting: entitiesService}, {provide: ListingComponent, useExisting: forwardRef(() => component)}, FilterService, SearchService, SortingService, ListingService]</code>
*/
export function listingProvidersFactory<T>(options: IListingServiceFactoryOptions<T>): Provider[];
export function listingProvidersFactory<T>(args?: IListingServiceFactoryOptions<T> | Type<unknown>): 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;
}

View File

@ -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<E extends Entity<I>, I> {
protected readonly _map = new Map<string, BehaviorSubject<E[]>>();
private readonly _entityChanged$ = new Subject<E>();
private readonly _entityDeleted$ = new Subject<E>();
protected abstract readonly _primaryKey: string;
protected constructor(@Inject('ENTITY_PRIMARY_KEY') protected readonly _primaryKey: string) {}
protected readonly _map = new Map<string, BehaviorSubject<E[]>>();
readonly #entityChanged$ = new Subject<E>();
readonly #entityDeleted$ = new Subject<E>();
get empty(): boolean {
return this._map.size === 0;
@ -45,7 +45,7 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
set(key: string, entities: E[]): void {
if (!this._map.has(key)) {
this._map.set(key, new BehaviorSubject<E[]>(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<E extends Entity<I>, 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<E extends Entity<I>, I> {
@Validate()
watch$(@RequiredParam() key: string, @RequiredParam() entityId: string): Observable<E> {
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<E extends Entity<I>, I> {
}
watchDeleted$(entityId: string): Observable<E> {
return this._entityDeleted$.pipe(filter(entity => entity.id === entityId));
return this.#entityDeleted$.pipe(filter(entity => entity.id === entityId));
}
private _pluckPrimaryKey(entity: E): string {

View File

@ -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<I> {
protected readonly _http = this._injector.get(HttpClient);
protected readonly _http = inject(HttpClient);
protected readonly _lastCheckedForChanges = new Map<string, string>([[ROOT_CHANGES_KEY, '0']]);
protected constructor(protected readonly _injector: Injector, protected readonly _defaultModelPath: string) {}
protected abstract readonly _defaultModelPath: string;
get<T = I[]>(): Observable<T>;
// eslint-disable-next-line @typescript-eslint/unified-signatures
get<T extends I[]>(): Observable<T>;
get<T = I>(id: string, ...args: unknown[]): Observable<T>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
get<T extends I>(id: string, ...args: unknown[]): Observable<T>;
get<T>(id?: string, ...args: unknown[]): Observable<T> {
return id ? this._getOne<T>([id]) : this.getAll<T>();
}
getAll<R extends I[]>(modelPath?: string, queryParams?: List<QueryParam>): Observable<R>;
getAll<R = I[]>(modelPath?: string, queryParams?: List<QueryParam>): Observable<R>;
getAll<R = I[]>(modelPath = this._defaultModelPath, queryParams?: List<QueryParam>): Observable<R> {
return this._http
.get<R>(`/${encodeURI(modelPath)}`, {
headers: HeadersConfiguration.getHeaders({ contentType: false }),
observe: 'body',
params: this._queryParams(queryParams),
})
.pipe(tap(() => this._updateLastChanged()));
const request$ = this._http.get<R>(`/${encodeURI(modelPath)}`, {
headers: HeadersConfiguration.getHeaders({ contentType: false }),
observe: 'body',
params: this._queryParams(queryParams),
});
return request$.pipe(tap(() => this._updateLastChanged()));
}
getFor<R = I[]>(entityId: string, queryParams?: List<QueryParam>): Observable<R> {

View File

@ -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<E, I = E> {
private readonly _http = this._injector.get(HttpClient);
private readonly _map = new Map<string, BehaviorSubject<E>>();
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<string, BehaviorSubject<E>>();
@Validate()
getFor(@RequiredParam() ids: string[]): Observable<E[]> {
const request = this._http.post<I[]>(`/${encodeURI(this._defaultModelPath)}`, ids, {
const request = this.#http.post<I[]>(`/${encodeURI(this._defaultModelPath)}`, ids, {
headers: HeadersConfiguration.getHeaders(),
observe: 'body',
});
@ -34,8 +31,8 @@ export abstract class StatsService<E, I = E> {
}
set(stats: E): void {
if (!this._map.has(this._pluckPrimaryKey(stats))) {
this._map.set(this._pluckPrimaryKey(stats), new BehaviorSubject<E>(stats));
if (!this.#map.has(this._pluckPrimaryKey(stats))) {
this.#map.set(this._pluckPrimaryKey(stats), new BehaviorSubject<E>(stats));
return;
}
@ -57,6 +54,6 @@ export abstract class StatsService<E, I = E> {
private _getBehaviourSubject(key: string): BehaviorSubject<E> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this._map.get(key)!;
return this.#map.get(key)!;
}
}

View File

@ -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 <T>(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);
}

View File

@ -21,7 +21,7 @@
"lib": ["es2021", "dom"],
"allowSyntheticDefaultImports": true
},
"include": ["**/*.ts"],
"include": ["src/**/*.ts"],
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,