Skip to content

Commit

Permalink
feat: Move to functional interceptors
Browse files Browse the repository at this point in the history
  • Loading branch information
mpalourdio committed Sep 11, 2024
1 parent c988fd9 commit dda6497
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 126 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## v16.1.0
- Add full standalone components support.
- Deprecate `NgHttpLoaderModule` and `forRoot`.
- `PendingRequestsInterceptor` has been refactored from `class` to `function`.
- Needed`getters` and `setters` have been extracted in a new `PendingRequestsInterceptorConfigurer`. If you had injected `PendingRequestsInterceptor` somewhere, you must switch to `PendingRequestsInterceptorConfigurer`. This is a needed BC break.
- Note that `^16.0.0` is the last release supporting `NgModule`.
- This is not semver compliant, I agree. But the direct usage of `PendingRequestsInterceptor` must be **very** low.

## v16.0.0
- Added angular 18 support.

Expand Down
66 changes: 17 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,46 +51,29 @@ If you experience errors like below, **please double-check the version you use.*

`ERROR in Error: Metadata version mismatch for module [...]/angular/node_modules/ng-http-loader/ng-http-loader.module.d.ts, found version x, expected y [...]`

## Requirements - HttpClientModule
## Requirements - HttpClient

Performing HTTP requests with the `HttpClientModule` API is **mandatory**. Otherwise, the spinner will not be fired **at all**.

See this [blog post](http://blog.ninja-squad.com/2017/07/17/http-client-module/) for an `HttpClientModule` introduction.
Performing HTTP requests with the `HttpClient` API is **mandatory**. Otherwise, the spinner will not be fired **at all**.

## Usage

From your Angular `AppModule`:
From your Angular root component (`app.component.ts` for example):

```typescript
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
// [...]
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http'; // <============
import { NgHttpLoaderModule } from 'ng-http-loader'; // <============

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule, // <============ (Perform HTTP requests with this module)
NgHttpLoaderModule.forRoot(), // <============ Don't forget to call 'forRoot()'!
],
providers: [],
bootstrap: [AppComponent]
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
imports: [NgHttpLoaderComponent] <====== import the component
})
export class AppModule { }
```

In your app.component.html, simply add:
In your `app.component.html`, simply add:
```xml
<ng-http-loader></ng-http-loader>
```
## Standalone components

If you prefer using standalone components, you should configure your `ApplicationConfig` like following:
At last, configure your `ApplicationConfig` like following:

```typescript
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
Expand All @@ -102,30 +85,11 @@ import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptorsFromDi() // <== Don't forget to import the interceptors
),
importProvidersFrom(NgHttpLoaderModule.forRoot()) //<== Always call `forRoot`
withInterceptors([pendingRequestsInterceptor$])
],
};
```
Then you can use `ng-http-loader` like this:
```typescript
import { Component } from '@angular/core';
import {NgHttpLoaderModule} from "ng-http-loader";

@Component({
selector: 'my-selector',
standalone: true,
imports: [
NgHttpLoaderModule
],
template: `
<ng-http-loader />`,
})
export class InlineComponent {
}
```
## Customizing the spinner

You can customize the following parameters:
Expand Down Expand Up @@ -159,8 +123,10 @@ import { Spinkit } from 'ng-http-loader'; // <============

@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
imports: [NgHttpLoaderComponent]
})
export class AppComponent {
public spinkit = Spinkit; // <============
Expand Down Expand Up @@ -188,8 +154,10 @@ import { AwesomeComponent } from 'my.awesome.component';

@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
styleUrls: ['./app.component.css'],
imports: [NgHttpLoaderComponent]
})
export class AppComponent {
public awesomeComponent = AwesomeComponent;
Expand Down
14 changes: 7 additions & 7 deletions src/lib/components/ng-http-loader.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
import { Component, Input, OnInit, Type } from '@angular/core';
import { merge, Observable, partition, timer } from 'rxjs';
import { debounce, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
import { PendingRequestsInterceptor } from '../services/pending-requests-interceptor.service';
import { SpinnerVisibilityService } from '../services/spinner-visibility.service';
import { Spinkit, SPINKIT_COMPONENTS } from '../spinkits';
import { AsyncPipe, NgComponentOutlet, NgIf, NgStyle } from "@angular/common";
import { PendingRequestsInterceptorConfigurer } from "../services/pending-requests-interceptor-configurer.service";

@Component({
selector: 'ng-http-loader',
Expand Down Expand Up @@ -41,7 +41,7 @@ export class NgHttpLoaderComponent implements OnInit {
@Input() backdropBackgroundColor = '#f1f1f1';
@Input() spinner: string | null = Spinkit.skWave;

constructor(private pendingRequestsInterceptor: PendingRequestsInterceptor, private spinnerVisibility: SpinnerVisibilityService) {
constructor(private pendingRequestsInterceptorConfigurer: PendingRequestsInterceptorConfigurer, private spinnerVisibility: SpinnerVisibilityService) {
}

ngOnInit(): void {
Expand All @@ -51,10 +51,10 @@ export class NgHttpLoaderComponent implements OnInit {
}

private initIsvisibleObservable(): void {
const [showSpinner$, hideSpinner$] = partition(this.pendingRequestsInterceptor.pendingRequestsStatus$, h => h);
const [showSpinner$, hideSpinner$] = partition(this.pendingRequestsInterceptorConfigurer.pendingRequestsStatus$, h => h);

this.isVisible$ = merge(
this.pendingRequestsInterceptor.pendingRequestsStatus$
this.pendingRequestsInterceptorConfigurer.pendingRequestsStatus$
.pipe(switchMap(() => showSpinner$.pipe(debounce(() => timer(this.debounceDelay))))),
showSpinner$
.pipe(switchMap(() => hideSpinner$.pipe(debounce(() => this.getVisibilityTimer$())))),
Expand All @@ -80,17 +80,17 @@ export class NgHttpLoaderComponent implements OnInit {
private initFilteredUrlPatterns(): void {
if (!!this.filteredUrlPatterns.length) {
this.filteredUrlPatterns.forEach(e =>
this.pendingRequestsInterceptor.filteredUrlPatterns.push(new RegExp(e))
this.pendingRequestsInterceptorConfigurer.filteredUrlPatterns.push(new RegExp(e))
);
}
}

private initFilteredMethods(): void {
this.pendingRequestsInterceptor.filteredMethods = this.filteredMethods;
this.pendingRequestsInterceptorConfigurer.filteredMethods = this.filteredMethods;
}

private initFilteredHeaders(): void {
this.pendingRequestsInterceptor.filteredHeaders = this.filteredHeaders;
this.pendingRequestsInterceptorConfigurer.filteredHeaders = this.filteredHeaders;
}

private updateExpirationDelay(showSpinner: boolean): void {
Expand Down
5 changes: 3 additions & 2 deletions src/lib/ng-http-loader.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
import { CommonModule } from '@angular/common';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { NgHttpLoaderComponent } from './components/ng-http-loader.component';
import { PendingRequestsInterceptorProvider } from './services/pending-requests-interceptor.service';
import { SPINKIT_COMPONENTS } from './spinkits';
import { pendingRequestsInterceptor$ } from "./services/pending-requests-interceptor";
import { provideHttpClient, withInterceptors } from "@angular/common/http";

/**
* @deprecated Will be removed in the next release, standalone component will become the default.
Expand All @@ -35,7 +36,7 @@ export class NgHttpLoaderModule {
return {
ngModule: NgHttpLoaderModule,
providers: [
PendingRequestsInterceptorProvider,
provideHttpClient(withInterceptors([pendingRequestsInterceptor$])),
]
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import { HTTP_INTERCEPTORS, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { ExistingProvider, Injectable } from '@angular/core';
import { HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, ReplaySubject } from 'rxjs';
import { finalize } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class PendingRequestsInterceptor implements HttpInterceptor {
export class PendingRequestsInterceptorConfigurer {

private _pendingRequests = 0;
private _pendingRequestsStatus$ = new ReplaySubject<boolean>(1);
Expand All @@ -28,10 +27,18 @@ export class PendingRequestsInterceptor implements HttpInterceptor {
return this._pendingRequestsStatus$.asObservable();
}

get pendingRequestsStatusSubject$(): ReplaySubject<boolean> {
return this._pendingRequestsStatus$;
}

get pendingRequests(): number {
return this._pendingRequests;
}

set pendingRequests(pendingRequests: number) {
this._pendingRequests = pendingRequests;
}

get filteredUrlPatterns(): RegExp[] {
return this._filteredUrlPatterns;
}
Expand All @@ -48,58 +55,28 @@ export class PendingRequestsInterceptor implements HttpInterceptor {
this._forceByPass = value;
}

private shouldBypassUrl(url: string): boolean {
shouldBypassUrl(url: string): boolean {
return this._filteredUrlPatterns.some(e => {
return e.test(url);
});
}

private shouldBypassMethod(req: HttpRequest<unknown>): boolean {
shouldBypassMethod(req: HttpRequest<unknown>): boolean {
return this._filteredMethods.some(e => {
return e.toUpperCase() === req.method.toUpperCase();
});
}

private shouldBypassHeader(req: HttpRequest<unknown>): boolean {
shouldBypassHeader(req: HttpRequest<unknown>): boolean {
return this._filteredHeaders.some(e => {
return req.headers.has(e);
});
}

private shouldBypass(req: HttpRequest<unknown>): boolean {
shouldBypass(req: HttpRequest<unknown>): boolean {
return this._forceByPass
|| this.shouldBypassUrl(req.urlWithParams)
|| this.shouldBypassMethod(req)
|| this.shouldBypassHeader(req);
}

intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const shouldBypass = this.shouldBypass(req);

if (!shouldBypass) {
this._pendingRequests++;

if (1 === this._pendingRequests) {
this._pendingRequestsStatus$.next(true);
}
}

return next.handle(req).pipe(
finalize(() => {
if (!shouldBypass) {
this._pendingRequests--;

if (0 === this._pendingRequests) {
this._pendingRequestsStatus$.next(false);
}
}
})
);
}
}

export const PendingRequestsInterceptorProvider: ExistingProvider[] = [{
provide: HTTP_INTERCEPTORS,
useExisting: PendingRequestsInterceptor,
multi: true
}];
39 changes: 39 additions & 0 deletions src/lib/services/pending-requests-interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import { HttpEvent, HttpHandlerFn, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { PendingRequestsInterceptorConfigurer } from "./pending-requests-interceptor-configurer.service";

export function pendingRequestsInterceptor$(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
const pendingRequestsInterceptorConfigurer = inject(PendingRequestsInterceptorConfigurer);
const shouldBypass = pendingRequestsInterceptorConfigurer.shouldBypass(req);

if (!shouldBypass) {
pendingRequestsInterceptorConfigurer.pendingRequests++;

if (1 === pendingRequestsInterceptorConfigurer.pendingRequests) {
pendingRequestsInterceptorConfigurer.pendingRequestsStatusSubject$.next(true);
}
}

return next(req).pipe(
finalize(() => {
if (!shouldBypass) {
pendingRequestsInterceptorConfigurer.pendingRequests--;

if (0 === pendingRequestsInterceptorConfigurer.pendingRequests) {
pendingRequestsInterceptorConfigurer.pendingRequestsStatusSubject$.next(false);
}
}
})
);
}
8 changes: 4 additions & 4 deletions src/lib/services/spinner-visibility.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { Injectable } from '@angular/core';
import { Observable, ReplaySubject } from 'rxjs';
import { PendingRequestsInterceptor } from './pending-requests-interceptor.service';
import { PendingRequestsInterceptorConfigurer } from "./pending-requests-interceptor-configurer.service";

@Injectable({
providedIn: 'root'
Expand All @@ -18,20 +18,20 @@ export class SpinnerVisibilityService {

private _visibility$ = new ReplaySubject<boolean>(1);

constructor(private pendingRequestsInterceptor: PendingRequestsInterceptor) {
constructor(private pendingRequestsInterceptorConfigurer: PendingRequestsInterceptorConfigurer) {
}

get visibility$(): Observable<boolean> {
return this._visibility$.asObservable();
}

show(): void {
this.pendingRequestsInterceptor.forceByPass = true;
this.pendingRequestsInterceptorConfigurer.forceByPass = true;
this._visibility$.next(true);
}

hide(): void {
this._visibility$.next(false);
this.pendingRequestsInterceptor.forceByPass = false;
this.pendingRequestsInterceptorConfigurer.forceByPass = false;
}
}
4 changes: 3 additions & 1 deletion src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export * from './lib/components/sk-wave/sk-wave.component';
export * from './lib/components/ng-http-loader.component';
export * from './lib/components/abstract.loader.directive';

export * from './lib/services/pending-requests-interceptor.service';
export * from './lib/services/pending-requests-interceptor';
export * from './lib/services/pending-requests-interceptor-configurer.service';

export * from './lib/services/spinner-visibility.service';

export * from './lib/ng-http-loader.module';
Expand Down
Loading

0 comments on commit dda6497

Please sign in to comment.