From 34c7dc73f6cfa62747dc4bcc75090262995de415 Mon Sep 17 00:00:00 2001 From: Boris Jotic Date: Thu, 3 Nov 2022 14:55:51 +0100 Subject: [PATCH 1/4] service, interceptor, models and testing component --- src/app/app-routing.module.ts | 12 +- src/app/app.component.ts | 3 +- src/app/app.module.ts | 22 ++-- .../http-loader.interceptor.spec.ts | 16 +++ .../interceptors/http-loader.interceptor.ts | 102 +++++++++++++++ src/app/core/interceptors/index.ts | 1 + src/app/core/services/index.ts | 0 .../services/loader-state.service.spec.ts | 16 +++ src/app/core/services/loader-state.service.ts | 95 ++++++++++++++ .../loader-test/loader-test.component.html | 5 + .../loader-test/loader-test.component.scss | 21 ++++ .../loader-test/loader-test.component.spec.ts | 25 ++++ src/app/loader-test/loader-test.component.ts | 119 ++++++++++++++++++ src/app/shared/enums/header-name.enum.ts | 4 + src/app/shared/enums/index.ts | 1 + src/app/shared/models/index.ts | 1 + src/app/shared/models/loader-header.model.ts | 3 + 17 files changed, 433 insertions(+), 13 deletions(-) create mode 100644 src/app/core/interceptors/http-loader.interceptor.spec.ts create mode 100644 src/app/core/interceptors/http-loader.interceptor.ts create mode 100644 src/app/core/interceptors/index.ts create mode 100644 src/app/core/services/index.ts create mode 100644 src/app/core/services/loader-state.service.spec.ts create mode 100644 src/app/core/services/loader-state.service.ts create mode 100644 src/app/loader-test/loader-test.component.html create mode 100644 src/app/loader-test/loader-test.component.scss create mode 100644 src/app/loader-test/loader-test.component.spec.ts create mode 100644 src/app/loader-test/loader-test.component.ts create mode 100644 src/app/shared/enums/header-name.enum.ts create mode 100644 src/app/shared/enums/index.ts create mode 100644 src/app/shared/models/index.ts create mode 100644 src/app/shared/models/loader-header.model.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 0297262..3487e71 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,10 +1,16 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { LoaderTestComponent } from './loader-test/loader-test.component'; -const routes: Routes = []; +const routes: Routes = [ + { + path: 'feat/loader-interceptor-service', + component: LoaderTestComponent, + }, +]; @NgModule({ imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] + exports: [RouterModule], }) -export class AppRoutingModule { } +export class AppRoutingModule {} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 57f36a6..8a0861a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -2,7 +2,8 @@ import { Component } from '@angular/core'; @Component({ selector: 'app-root', - templateUrl: './app.component.html', + // templateUrl: './app.component.html', + template: ``, styleUrls: ['./app.component.scss'] }) export class AppComponent { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b1c6c96..3695848 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,18 +1,22 @@ +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; +import { HttPLoaderInterceptor } from './core/interceptors'; +import { LoaderTestComponent } from './loader-test/loader-test.component'; @NgModule({ - declarations: [ - AppComponent + declarations: [AppComponent, LoaderTestComponent], + imports: [BrowserModule, AppRoutingModule, HttpClientModule], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: HttPLoaderInterceptor, + multi: true, + }, ], - imports: [ - BrowserModule, - AppRoutingModule - ], - providers: [], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule {} diff --git a/src/app/core/interceptors/http-loader.interceptor.spec.ts b/src/app/core/interceptors/http-loader.interceptor.spec.ts new file mode 100644 index 0000000..9ddea8b --- /dev/null +++ b/src/app/core/interceptors/http-loader.interceptor.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { HttPLoaderInterceptor } from './http-loader.interceptor'; + +describe('HttLoaderInterceptor', () => { + beforeEach(() => TestBed.configureTestingModule({ + providers: [ + HttPLoaderInterceptor + ] + })); + + it('should be created', () => { + const interceptor: HttPLoaderInterceptor = TestBed.inject(HttPLoaderInterceptor); + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/src/app/core/interceptors/http-loader.interceptor.ts b/src/app/core/interceptors/http-loader.interceptor.ts new file mode 100644 index 0000000..86bd988 --- /dev/null +++ b/src/app/core/interceptors/http-loader.interceptor.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor, +} from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { HeaderName } from 'src/app/shared/enums'; +import { finalize } from 'rxjs/operators'; +import { LoaderStateServiceService } from '../services/loader-state.service'; + +@Injectable() +export class HttPLoaderInterceptor implements HttpInterceptor { + /** + * Array of requests that are ignored by default, e.g. login, logout, etc... + */ + private readonly requestForIgnore = ['v1/login', 'v2/logut']; + + constructor(private loaderStateService: LoaderStateServiceService) {} + + intercept( + request: HttpRequest, + next: HttpHandler + ): Observable> { + const req = + this.handleIgnoredRequest(request, next) || + this.handleUserIgnoredRequest(request, next) || + this.handleDedicatedRequest(request, next) || + this.handleRequestWithLoader(request, next); + + return req; + } + + private handleUserIgnoredRequest( + request: HttpRequest, + next: HttpHandler + ): Observable> | null { + let output = null; + + if (request.headers.get(HeaderName.userIgnoredLoader)) { + const req = request.clone({ + headers: request.headers.delete(HeaderName.userIgnoredLoader), + }); + + output = next.handle(req); + } + + return output; + } + + private handleDedicatedRequest( + request: HttpRequest, + next: HttpHandler + ): Observable> | null { + let output = null; + const key = request.headers.get(HeaderName.showDedicatedLoader); + + if (key) { + const req = request.clone({ + headers: request.headers.delete(HeaderName.showDedicatedLoader), + }); + + this.loaderStateService.showDedicatedFor(key); + output = next + .handle(req) + .pipe(finalize(() => this.loaderStateService.hideDedicatedFor(key))); + } + + return output; + } + + private handleIgnoredRequest( + request: HttpRequest, + next: HttpHandler + ): Observable> | null { + let output = next.handle(request); + const url = request.url.toLowerCase(); + + const requestIgnored = + this.requestForIgnore.filter((ignoreReq) => + ignoreReq.toLowerCase().endsWith(url) + ).length === 0; + + if (requestIgnored) { + output = null; + } + + return output; + } + + private handleRequestWithLoader( + request: HttpRequest, + next: HttpHandler + ): Observable> { + this.loaderStateService.showMain(); + + return next + .handle(request) + .pipe(finalize(() => this.loaderStateService.hideMain())); + } +} diff --git a/src/app/core/interceptors/index.ts b/src/app/core/interceptors/index.ts new file mode 100644 index 0000000..86981ce --- /dev/null +++ b/src/app/core/interceptors/index.ts @@ -0,0 +1 @@ +export * from './http-loader.interceptor'; diff --git a/src/app/core/services/index.ts b/src/app/core/services/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/services/loader-state.service.spec.ts b/src/app/core/services/loader-state.service.spec.ts new file mode 100644 index 0000000..67690b8 --- /dev/null +++ b/src/app/core/services/loader-state.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { LoaderStateServiceService } from './loader-state.service'; + +describe('LoaderStateServiceService', () => { + let service: LoaderStateServiceService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LoaderStateServiceService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/loader-state.service.ts b/src/app/core/services/loader-state.service.ts new file mode 100644 index 0000000..9d09f4a --- /dev/null +++ b/src/app/core/services/loader-state.service.ts @@ -0,0 +1,95 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map, skip } from 'rxjs/operators'; +import { HeaderName } from 'src/app/shared/enums'; +import { LoaderHeader } from 'src/app/shared/models'; + +@Injectable({ + providedIn: 'root', +}) +export class LoaderStateServiceService { + private mainLoaderState$ = new BehaviorSubject(0); + private dedicatedLoaderState$ = new BehaviorSubject([]); + + //#region Getters for main/dedicated loader + + public get showMainLoader$(): Observable { + return this.mainLoaderState$.asObservable().pipe( + skip(1), // we're not interested in the default state + map((currentState: number) => currentState !== 0) + ); + } + + public get dedicatedLoaders$(): Observable { + return this.dedicatedLoaderState$.asObservable(); + } + + public dedicatedLoaderFor(key: string): Observable { + return this.dedicatedLoaders$.pipe( + map((keys: string[]) => keys.indexOf(key) !== -1) + ); + } + + //#endregion + + //#region Toggling methods + + public showMain(): void { + let current = this.mainLoaderState$.getValue(); + + this.mainLoaderState$.next(++current); + } + + public hideMain(): void { + let current = this.mainLoaderState$.getValue(); + + this.mainLoaderState$.next(--current); + } + + public showDedicatedFor(key: string): void { + let current = this.dedicatedLoaderState$.getValue(); + + this.dedicatedLoaderState$.next([...current, key]); + } + + public hideDedicatedFor(key: string): void { + let current = this.dedicatedLoaderState$.getValue(); + + const index = current.indexOf(key); + current.splice(index, 1); + + this.dedicatedLoaderState$.next(current); + } + + //#endregion + + //#region Header utility methods + + public generateHeaderFor(names: HeaderName[]): LoaderHeader { + const output: LoaderHeader = {}; + + const nonDefaultValues = new Map([ + [ + HeaderName.showDedicatedLoader, + `present-and-unique-key-${Math.random()}`, // @ervin: maybe Math.random() isn't unique enough? + ], + ]); + + for (let headerName of names) { + output[headerName] = nonDefaultValues.get(headerName) ?? 'present'; + } + + return output; + } + + public initLoaderForDedicatedHeader(): [LoaderHeader, Observable] { + const header = this.generateHeaderFor([HeaderName.showDedicatedLoader]); + + return [ + header, + this.dedicatedLoaderFor(header[HeaderName.showDedicatedLoader]), + ]; + } + + //#endregion +} diff --git a/src/app/loader-test/loader-test.component.html b/src/app/loader-test/loader-test.component.html new file mode 100644 index 0000000..e206549 --- /dev/null +++ b/src/app/loader-test/loader-test.component.html @@ -0,0 +1,5 @@ +

loader-test works!

+ +
+ +
diff --git a/src/app/loader-test/loader-test.component.scss b/src/app/loader-test/loader-test.component.scss new file mode 100644 index 0000000..1e7db91 --- /dev/null +++ b/src/app/loader-test/loader-test.component.scss @@ -0,0 +1,21 @@ +#main-loader { + display: block; + width: 500px; + height: 500px; + background-color: aqua; + position: absolute; + top: 0px; + left: 0px; + margin-top: 50px; +} + +#dedicated-loader { + display: block; + width: 200px; + height: 200px; + background-color: olive; + position: absolute; + top: 500px; + left: 0px; + margin-top: 50px; +} diff --git a/src/app/loader-test/loader-test.component.spec.ts b/src/app/loader-test/loader-test.component.spec.ts new file mode 100644 index 0000000..572026e --- /dev/null +++ b/src/app/loader-test/loader-test.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoaderTestComponent } from './loader-test.component'; + +describe('LoaderTestComponent', () => { + let component: LoaderTestComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LoaderTestComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LoaderTestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/loader-test/loader-test.component.ts b/src/app/loader-test/loader-test.component.ts new file mode 100644 index 0000000..5dcffd0 --- /dev/null +++ b/src/app/loader-test/loader-test.component.ts @@ -0,0 +1,119 @@ +import { HttpClient } from '@angular/common/http'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnInit, +} from '@angular/core'; +import { Observable } from 'rxjs'; +import { LoaderStateServiceService } from '../core/services/loader-state.service'; +import { HeaderName } from '../shared/enums'; +import { LoaderHeader } from '../shared/models'; + +@Component({ + selector: 'app-loader-test', + templateUrl: './loader-test.component.html', + styleUrls: ['./loader-test.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoaderTestComponent implements OnInit { + public loader$ = this.loaderStateService.showMainLoader$; + public dedicatedLoader$: Observable; + + private dedicatedHeader: LoaderHeader; + + constructor( + private httpClient: HttpClient, + private loaderStateService: LoaderStateServiceService, + private cdr: ChangeDetectorRef + ) {} + //http://dev.qposoft.com:4082/api/users + + ngOnInit(): void { + // just test loaders + setTimeout(() => this.initFakeLoaders(), 1_500); + + // test main loader + setTimeout(() => this.initMainLoader(), 3_000); + + // test dedicated loader + setTimeout( + () => this.initDedicatedHeaderLoader().initDedicatedLoader(), + 3_500 + ); + + // test w/o loader + setTimeout(() => this.initRequestWithoutLoader(), 4_500); + } + + private initFakeLoaders(): this { + const dedicatedLoaderKey = 'lorem-ipsum'; + + this.loaderStateService.showMain(); + this.loaderStateService.showDedicatedFor(dedicatedLoaderKey); + this.dedicatedLoader$ = + this.loaderStateService.dedicatedLoaderFor(dedicatedLoaderKey); + + setTimeout(() => { + this.loaderStateService.hideMain(); + }, 500); + setTimeout( + () => this.loaderStateService.hideDedicatedFor(dedicatedLoaderKey), + 700 + ); + + return this; + } + + private initRequestWithoutLoader(): this { + this.dedicatedHeader = this.loaderStateService.generateHeaderFor([ + HeaderName.userIgnoredLoader, + ]); + + this.httpClient + .get('http://dev.qposoft.com:4082/api/users', { + headers: this.dedicatedHeader, + }) + .subscribe(console.log); + + return this; + } + + private initDedicatedLoader(): this { + this.httpClient + .get('http://dev.qposoft.com:4082/api/users', { + headers: this.dedicatedHeader, + }) + .subscribe(console.log); + + return this; + } + + private initDedicatedHeaderLoader(): this { + [this.dedicatedHeader, this.dedicatedLoader$] = + this.loaderStateService.initLoaderForDedicatedHeader(); + + /** + * HEADS UP: + * Because of the "changeDetection: ChangeDetectionStrategy.OnPush" + * inside component decorator, above line is updated property that + * is used inside tpl, and we need to manually trigger update. + * When we remove OnPush this isn't the case. + * But, IMHO there is no need to worry about this code, because, + * as Nebojsa suggested, we should create directive for dedicated loader, + * so this piece of code will be part only of that directive, and it should + * receive as @input value for showing/hiding dedicated loader. + */ + this.cdr.detectChanges(); + + return this; + } + + private initMainLoader(): this { + this.httpClient + .get('http://dev.qposoft.com:4082/api/users') + .subscribe(console.log); + + return this; + } +} diff --git a/src/app/shared/enums/header-name.enum.ts b/src/app/shared/enums/header-name.enum.ts new file mode 100644 index 0000000..ec88a66 --- /dev/null +++ b/src/app/shared/enums/header-name.enum.ts @@ -0,0 +1,4 @@ +export enum HeaderName { + userIgnoredLoader = 'EY-Maestro-User-Ignored-Loader', + showDedicatedLoader = 'EY-Maestro-Show-Dedicated-Loader', +} diff --git a/src/app/shared/enums/index.ts b/src/app/shared/enums/index.ts new file mode 100644 index 0000000..2586c51 --- /dev/null +++ b/src/app/shared/enums/index.ts @@ -0,0 +1 @@ +export * from './header-name.enum'; diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts new file mode 100644 index 0000000..9fba242 --- /dev/null +++ b/src/app/shared/models/index.ts @@ -0,0 +1 @@ +export * from './loader-header.model'; diff --git a/src/app/shared/models/loader-header.model.ts b/src/app/shared/models/loader-header.model.ts new file mode 100644 index 0000000..905e9f2 --- /dev/null +++ b/src/app/shared/models/loader-header.model.ts @@ -0,0 +1,3 @@ +import { HeaderName } from '../enums/header-name.enum'; + +export type LoaderHeader = { [key in HeaderName]?: string }; From 1824b2646d2afd230a59bec70337bdaa72d30650 Mon Sep 17 00:00:00 2001 From: Boris Jotic Date: Tue, 8 Nov 2022 11:14:39 +0100 Subject: [PATCH 2/4] various improvements and code tweaks w/ refactoring --- src/app/app.module.ts | 7 +- .../dedicated-loader.directive.spec.ts | 8 + .../directives/dedicated-loader.directive.ts | 20 +++ src/app/core/directives/index.ts | 1 + .../http-loader.interceptor.spec.ts | 6 +- .../interceptors/http-loader.interceptor.ts | 10 +- .../services/loader-state.service.spec.ts | 6 +- src/app/core/services/loader-state.service.ts | 106 ++++++++++--- .../loader-test/loader-test.component.html | 52 +++++- .../loader-test/loader-test.component.scss | 18 +-- src/app/loader-test/loader-test.component.ts | 149 ++++++++++++++++-- src/app/shared/enums/header-name.enum.ts | 4 +- src/styles.scss | 4 + 13 files changed, 322 insertions(+), 69 deletions(-) create mode 100644 src/app/core/directives/dedicated-loader.directive.spec.ts create mode 100644 src/app/core/directives/dedicated-loader.directive.ts create mode 100644 src/app/core/directives/index.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3695848..ee1e003 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -4,16 +4,17 @@ import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { HttPLoaderInterceptor } from './core/interceptors'; +import { DedicatedLoaderDirective } from './core/directives'; +import { HttpLoaderInterceptor } from './core/interceptors'; import { LoaderTestComponent } from './loader-test/loader-test.component'; @NgModule({ - declarations: [AppComponent, LoaderTestComponent], + declarations: [AppComponent, LoaderTestComponent, DedicatedLoaderDirective], imports: [BrowserModule, AppRoutingModule, HttpClientModule], providers: [ { provide: HTTP_INTERCEPTORS, - useClass: HttPLoaderInterceptor, + useClass: HttpLoaderInterceptor, multi: true, }, ], diff --git a/src/app/core/directives/dedicated-loader.directive.spec.ts b/src/app/core/directives/dedicated-loader.directive.spec.ts new file mode 100644 index 0000000..d7203b8 --- /dev/null +++ b/src/app/core/directives/dedicated-loader.directive.spec.ts @@ -0,0 +1,8 @@ +import { DedicatedLoaderDirective } from './dedicated-loader.directive'; + +describe('DedicatedLoaderDirective', () => { + it('should create an instance', () => { + const directive = new DedicatedLoaderDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/app/core/directives/dedicated-loader.directive.ts b/src/app/core/directives/dedicated-loader.directive.ts new file mode 100644 index 0000000..627fd89 --- /dev/null +++ b/src/app/core/directives/dedicated-loader.directive.ts @@ -0,0 +1,20 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; + +@Directive({ + selector: '[appDedicatedLoader]', +}) +export class DedicatedLoaderDirective { + private readonly loaderClass = 'directive-loader'; // defined in styles.scss + + @Input() set loaderStatus(value: boolean) { + if (this.el.nativeElement) { + if (value) { + this.el.nativeElement.classList.add(this.loaderClass); + } else { + this.el.nativeElement.classList.remove(this.loaderClass); + } + } + } + + constructor(private el: ElementRef) {} +} diff --git a/src/app/core/directives/index.ts b/src/app/core/directives/index.ts new file mode 100644 index 0000000..fdeba89 --- /dev/null +++ b/src/app/core/directives/index.ts @@ -0,0 +1 @@ +export * from './dedicated-loader.directive'; diff --git a/src/app/core/interceptors/http-loader.interceptor.spec.ts b/src/app/core/interceptors/http-loader.interceptor.spec.ts index 9ddea8b..34616c7 100644 --- a/src/app/core/interceptors/http-loader.interceptor.spec.ts +++ b/src/app/core/interceptors/http-loader.interceptor.spec.ts @@ -1,16 +1,16 @@ import { TestBed } from '@angular/core/testing'; -import { HttPLoaderInterceptor } from './http-loader.interceptor'; +import { HttpLoaderInterceptor } from './http-loader.interceptor'; describe('HttLoaderInterceptor', () => { beforeEach(() => TestBed.configureTestingModule({ providers: [ - HttPLoaderInterceptor + HttpLoaderInterceptor ] })); it('should be created', () => { - const interceptor: HttPLoaderInterceptor = TestBed.inject(HttPLoaderInterceptor); + const interceptor: HttpLoaderInterceptor = TestBed.inject(HttpLoaderInterceptor); expect(interceptor).toBeTruthy(); }); }); diff --git a/src/app/core/interceptors/http-loader.interceptor.ts b/src/app/core/interceptors/http-loader.interceptor.ts index 86bd988..ad6272f 100644 --- a/src/app/core/interceptors/http-loader.interceptor.ts +++ b/src/app/core/interceptors/http-loader.interceptor.ts @@ -8,16 +8,16 @@ import { import { Observable } from 'rxjs'; import { HeaderName } from 'src/app/shared/enums'; import { finalize } from 'rxjs/operators'; -import { LoaderStateServiceService } from '../services/loader-state.service'; +import { LoaderStateService } from '../services/loader-state.service'; @Injectable() -export class HttPLoaderInterceptor implements HttpInterceptor { +export class HttpLoaderInterceptor implements HttpInterceptor { /** * Array of requests that are ignored by default, e.g. login, logout, etc... */ private readonly requestForIgnore = ['v1/login', 'v2/logut']; - constructor(private loaderStateService: LoaderStateServiceService) {} + constructor(private loaderStateService: LoaderStateService) {} intercept( request: HttpRequest, @@ -93,10 +93,10 @@ export class HttPLoaderInterceptor implements HttpInterceptor { request: HttpRequest, next: HttpHandler ): Observable> { - this.loaderStateService.showMain(); + const uniqueKey = this.loaderStateService.showMain(); return next .handle(request) - .pipe(finalize(() => this.loaderStateService.hideMain())); + .pipe(finalize(() => this.loaderStateService.hideMain(uniqueKey))); } } diff --git a/src/app/core/services/loader-state.service.spec.ts b/src/app/core/services/loader-state.service.spec.ts index 67690b8..7245ea1 100644 --- a/src/app/core/services/loader-state.service.spec.ts +++ b/src/app/core/services/loader-state.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { LoaderStateServiceService } from './loader-state.service'; +import { LoaderStateService } from './loader-state.service'; describe('LoaderStateServiceService', () => { - let service: LoaderStateServiceService; + let service: LoaderStateService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(LoaderStateServiceService); + service = TestBed.inject(LoaderStateService); }); it('should be created', () => { diff --git a/src/app/core/services/loader-state.service.ts b/src/app/core/services/loader-state.service.ts index 9d09f4a..349b13c 100644 --- a/src/app/core/services/loader-state.service.ts +++ b/src/app/core/services/loader-state.service.ts @@ -4,29 +4,46 @@ import { map, skip } from 'rxjs/operators'; import { HeaderName } from 'src/app/shared/enums'; import { LoaderHeader } from 'src/app/shared/models'; +type LoaderKeyTimeoutState = { [uniqueKey: string]: number }; + +// todo: +// - navigation & tab change +// - ngb-nav + @Injectable({ providedIn: 'root', }) -export class LoaderStateServiceService { - private mainLoaderState$ = new BehaviorSubject(0); - private dedicatedLoaderState$ = new BehaviorSubject([]); +export class LoaderStateService { + //#region Class properties + + private readonly maxRequestLength = 61_000; // [ms]; + + private mainLoaderState$ = new BehaviorSubject({}); + private dedicatedLoaderState$ = new BehaviorSubject( + {} + ); + + //#endregion //#region Getters for main/dedicated loader public get showMainLoader$(): Observable { return this.mainLoaderState$.asObservable().pipe( skip(1), // we're not interested in the default state - map((currentState: number) => currentState !== 0) + map( + (currentState: LoaderKeyTimeoutState) => + Object.keys(currentState).length !== 0 + ) ); } - public get dedicatedLoaders$(): Observable { + public get dedicatedLoaders$(): Observable { return this.dedicatedLoaderState$.asObservable(); } public dedicatedLoaderFor(key: string): Observable { return this.dedicatedLoaders$.pipe( - map((keys: string[]) => keys.indexOf(key) !== -1) + map((state: LoaderKeyTimeoutState) => key in state) ); } @@ -34,31 +51,25 @@ export class LoaderStateServiceService { //#region Toggling methods - public showMain(): void { - let current = this.mainLoaderState$.getValue(); + public showMain(key?: string): string { + if (!key) { + key = this.generateUniqueKey(); + } + this.modifyStateViaAdd(this.mainLoaderState$, key, 'main'); - this.mainLoaderState$.next(++current); + return key; } - public hideMain(): void { - let current = this.mainLoaderState$.getValue(); - - this.mainLoaderState$.next(--current); + public hideMain(key: string): void { + this.modifyStateViaRmv(this.mainLoaderState$, key); } public showDedicatedFor(key: string): void { - let current = this.dedicatedLoaderState$.getValue(); - - this.dedicatedLoaderState$.next([...current, key]); + this.modifyStateViaAdd(this.dedicatedLoaderState$, key, 'dedicated'); } public hideDedicatedFor(key: string): void { - let current = this.dedicatedLoaderState$.getValue(); - - const index = current.indexOf(key); - current.splice(index, 1); - - this.dedicatedLoaderState$.next(current); + this.modifyStateViaRmv(this.dedicatedLoaderState$, key); } //#endregion @@ -69,10 +80,7 @@ export class LoaderStateServiceService { const output: LoaderHeader = {}; const nonDefaultValues = new Map([ - [ - HeaderName.showDedicatedLoader, - `present-and-unique-key-${Math.random()}`, // @ervin: maybe Math.random() isn't unique enough? - ], + [HeaderName.showDedicatedLoader, this.generateUniqueKey()], ]); for (let headerName of names) { @@ -92,4 +100,50 @@ export class LoaderStateServiceService { } //#endregion + + //#region State utility methods + + private modifyStateViaAdd( + state$: BehaviorSubject, + key: string, + state: 'main' | 'dedicated' = 'main' + ): LoaderKeyTimeoutState { + const current = state$.getValue(); + + current[key] = window.setTimeout( + () => + state === 'main' ? this.hideMain(key) : this.hideDedicatedFor(key), + this.maxRequestLength + ); + + state$.next(current); + + return current; + } + + private modifyStateViaRmv( + state$: BehaviorSubject, + key: string + ): LoaderKeyTimeoutState { + const current = state$.getValue(); + + if (current[key]) { + window.clearTimeout(current[key]); + delete current[key]; + + state$.next(current); + } + + return current; + } + + //#endregion + + //#region Utility methods + + public generateUniqueKey(): string { + return `present-and-unique-key-${Date.now() + Math.random()}`; + } + + //#endregion } diff --git a/src/app/loader-test/loader-test.component.html b/src/app/loader-test/loader-test.component.html index e206549..2231573 100644 --- a/src/app/loader-test/loader-test.component.html +++ b/src/app/loader-test/loader-test.component.html @@ -1,5 +1,53 @@

loader-test works!

-
+
+
+
main state
+ {{ mainState$ | async | json }} +
+
+
dedicated state:
+ {{ dedicatedState$ | async | json }} +
+
-
+
+
+ +
+ + + +
+ + + + +
+ + diff --git a/src/app/loader-test/loader-test.component.scss b/src/app/loader-test/loader-test.component.scss index 1e7db91..d89087f 100644 --- a/src/app/loader-test/loader-test.component.scss +++ b/src/app/loader-test/loader-test.component.scss @@ -1,21 +1,19 @@ #main-loader { display: block; - width: 500px; - height: 500px; - background-color: aqua; + width: 100vw; + height: 50px; + background-color: darkcyan; position: absolute; - top: 0px; + bottom: 0px; left: 0px; - margin-top: 50px; } #dedicated-loader { display: block; - width: 200px; - height: 200px; - background-color: olive; + width: 100vw; + height: 25px; + background-color: chocolate; position: absolute; - top: 500px; + bottom: 50px; left: 0px; - margin-top: 50px; } diff --git a/src/app/loader-test/loader-test.component.ts b/src/app/loader-test/loader-test.component.ts index 5dcffd0..5e47981 100644 --- a/src/app/loader-test/loader-test.component.ts +++ b/src/app/loader-test/loader-test.component.ts @@ -6,7 +6,7 @@ import { OnInit, } from '@angular/core'; import { Observable } from 'rxjs'; -import { LoaderStateServiceService } from '../core/services/loader-state.service'; +import { LoaderStateService } from '../core/services/loader-state.service'; import { HeaderName } from '../shared/enums'; import { LoaderHeader } from '../shared/models'; @@ -17,6 +17,11 @@ import { LoaderHeader } from '../shared/models'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class LoaderTestComponent implements OnInit { + //#region Don't do this at home + public mainState$ = this.loaderStateService['mainLoaderState$']; + public dedicatedState$ = this.loaderStateService['dedicatedLoaderState$']; + //#endregion + public loader$ = this.loaderStateService.showMainLoader$; public dedicatedLoader$: Observable; @@ -24,38 +29,40 @@ export class LoaderTestComponent implements OnInit { constructor( private httpClient: HttpClient, - private loaderStateService: LoaderStateServiceService, + private loaderStateService: LoaderStateService, private cdr: ChangeDetectorRef ) {} - //http://dev.qposoft.com:4082/api/users ngOnInit(): void { // just test loaders - setTimeout(() => this.initFakeLoaders(), 1_500); + // setTimeout(() => this.initFakeLoaders(), 1_500); // test main loader - setTimeout(() => this.initMainLoader(), 3_000); + // setTimeout(() => this.initMainLoader(), 3_000); // test dedicated loader - setTimeout( - () => this.initDedicatedHeaderLoader().initDedicatedLoader(), - 3_500 - ); + // setTimeout( + // () => this.initDedicatedHeaderLoader().initDedicatedLoader(), + // 3_500 + // ); // test w/o loader - setTimeout(() => this.initRequestWithoutLoader(), 4_500); + // setTimeout(() => this.initRequestWithoutLoader(), 4_500); } + //#region Init methods + private initFakeLoaders(): this { const dedicatedLoaderKey = 'lorem-ipsum'; + const mainLoaderKey = 'aaaaaa'; - this.loaderStateService.showMain(); + this.loaderStateService.showMain(mainLoaderKey); this.loaderStateService.showDedicatedFor(dedicatedLoaderKey); this.dedicatedLoader$ = this.loaderStateService.dedicatedLoaderFor(dedicatedLoaderKey); setTimeout(() => { - this.loaderStateService.hideMain(); + this.loaderStateService.hideMain(mainLoaderKey); }, 500); setTimeout( () => this.loaderStateService.hideDedicatedFor(dedicatedLoaderKey), @@ -80,9 +87,16 @@ export class LoaderTestComponent implements OnInit { } private initDedicatedLoader(): this { + const uniqueId = this.loaderStateService.generateUniqueKey(); + this.dedicatedLoader$ = + this.loaderStateService.dedicatedLoaderFor(uniqueId); + this.httpClient - .get('http://dev.qposoft.com:4082/api/users', { - headers: this.dedicatedHeader, + // .get('http://dev.qposoft.com:4082/api/users', { + .get('http://dev.qposoft.com:4082/api/sleep/3', { + headers: { + [HeaderName.showDedicatedLoader]: uniqueId, + }, }) .subscribe(console.log); @@ -90,6 +104,7 @@ export class LoaderTestComponent implements OnInit { } private initDedicatedHeaderLoader(): this { + return this; [this.dedicatedHeader, this.dedicatedLoader$] = this.loaderStateService.initLoaderForDedicatedHeader(); @@ -111,9 +126,113 @@ export class LoaderTestComponent implements OnInit { private initMainLoader(): this { this.httpClient - .get('http://dev.qposoft.com:4082/api/users') + .get('http://dev.qposoft.com:4082/api/sleep/5') .subscribe(console.log); return this; } + + //#endregion + + //#region UI events + + public async onTestMainLoader(): Promise { + this.initMainLoader(); + } + + //#endregion + + //#region Ad-hock dedicated loaders testing + + public dedicatedLoader1$: Observable; + public dedicatedLoader2$: Observable; + public async onTestDedicatedLoader(id: string): Promise { + const uniqueId = this.loaderStateService.generateUniqueKey() + id; + if (id === '1') { + this.dedicatedLoader1$ = + this.loaderStateService.dedicatedLoaderFor(uniqueId); + } else { + this.dedicatedLoader2$ = + this.loaderStateService.dedicatedLoaderFor(uniqueId); + } + + this.httpClient + // .get('http://dev.qposoft.com:4082/api/users', { + .get('http://dev.qposoft.com:4082/api/sleep/3', { + headers: { + [HeaderName.showDedicatedLoader]: uniqueId, + }, + }) + .subscribe(console.log); + } + + //#endregion + + //#region Proposals on how to use dedicated loader + + public dedicatedLoaderVisible$: Observable; + + public async onDedicatedLoaderEventProposal1(): Promise { + // prepare unique id + const uniqueId = this.loaderStateService.generateUniqueKey(); + // prepare loader visibility Observable + this.dedicatedLoaderVisible$ = + this.loaderStateService.dedicatedLoaderFor(uniqueId); + // make call + this.httpClient + .get('http://dev.qposoft.com:4082/api/sleep/3', { + headers: { + [HeaderName.showDedicatedLoader]: uniqueId, + }, + }) + .subscribe(console.log); + } + + public async onDedicatedLoaderEventProposal2(): Promise { + // prepare unique id + const uniqueId = `some-random-and-unique-id-${Date.now()}`; + // prepare loader visibility Observable + this.dedicatedLoaderVisible$ = + this.loaderStateService.dedicatedLoaderFor(uniqueId); + // make call + this.httpClient + .get('http://dev.qposoft.com:4082/api/sleep/3', { + headers: { + [HeaderName.showDedicatedLoader]: uniqueId, + }, + }) + .subscribe(console.log); + } + + public async onDedicatedLoaderEventProposal3(): Promise { + // prepare header and unique id + const headers = this.loaderStateService.generateHeaderFor([ + HeaderName.showDedicatedLoader, + ]); + // prepare visibility Observable + this.dedicatedLoaderVisible$ = this.loaderStateService.dedicatedLoaderFor( + headers[HeaderName.showDedicatedLoader] + ); + // make call + this.httpClient + .get('http://dev.qposoft.com:4082/api/sleep/3', { + headers, + }) + .subscribe(console.log); + } + + public async onDedicatedLoaderEventProposal4(): Promise { + // prepare header, unique id and visibility Observable + let headers = {}; + [headers, this.dedicatedLoaderVisible$] = + this.loaderStateService.initLoaderForDedicatedHeader(); + // make call + this.httpClient + .get('http://dev.qposoft.com:4082/api/sleep/3', { + headers, + }) + .subscribe(console.log); + } + + //#endregion } diff --git a/src/app/shared/enums/header-name.enum.ts b/src/app/shared/enums/header-name.enum.ts index ec88a66..a52de38 100644 --- a/src/app/shared/enums/header-name.enum.ts +++ b/src/app/shared/enums/header-name.enum.ts @@ -1,4 +1,4 @@ export enum HeaderName { - userIgnoredLoader = 'EY-Maestro-User-Ignored-Loader', - showDedicatedLoader = 'EY-Maestro-Show-Dedicated-Loader', + userIgnoredLoader = 'PROJECT-User-Ignored-Loader', + showDedicatedLoader = 'PROJECT-Show-Dedicated-Loader', } diff --git a/src/styles.scss b/src/styles.scss index 90d4ee0..76e3a2c 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1 +1,5 @@ /* You can add global styles to this file, and also import other style files */ +.directive-loader { + background-color: chocolate; + color: white; +} From 6a3de7f5a7553d826bc49faad96b1a6b085b6fd2 Mon Sep 17 00:00:00 2001 From: Boris Jotic Date: Tue, 8 Nov 2022 11:32:35 +0100 Subject: [PATCH 3/4] updated routing module so it will open testing page --- src/app/app-routing.module.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 3487e71..3548250 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -7,6 +7,10 @@ const routes: Routes = [ path: 'feat/loader-interceptor-service', component: LoaderTestComponent, }, + { + path: '**', + redirectTo: 'feat/loader-interceptor-service', + }, ]; @NgModule({ From 107ac65452aa96f08fd51226e24edd3917e452a3 Mon Sep 17 00:00:00 2001 From: Boris Jotic Date: Tue, 8 Nov 2022 15:18:47 +0100 Subject: [PATCH 4/4] comment change and fixed logic for ignored requests --- src/app/core/interceptors/http-loader.interceptor.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/core/interceptors/http-loader.interceptor.ts b/src/app/core/interceptors/http-loader.interceptor.ts index ad6272f..4baf0a5 100644 --- a/src/app/core/interceptors/http-loader.interceptor.ts +++ b/src/app/core/interceptors/http-loader.interceptor.ts @@ -77,10 +77,9 @@ export class HttpLoaderInterceptor implements HttpInterceptor { let output = next.handle(request); const url = request.url.toLowerCase(); - const requestIgnored = - this.requestForIgnore.filter((ignoreReq) => - ignoreReq.toLowerCase().endsWith(url) - ).length === 0; + const requestIgnored = !this.requestForIgnore.filter((ignoreReq) => + url.toLowerCase().endsWith(ignoreReq) + ).length; if (requestIgnored) { output = null;