Skip to content

Commit dda6497

Browse files
committed
feat: Move to functional interceptors
1 parent c988fd9 commit dda6497

12 files changed

+115
-126
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## v16.1.0
4+
- Add full standalone components support.
5+
- Deprecate `NgHttpLoaderModule` and `forRoot`.
6+
- `PendingRequestsInterceptor` has been refactored from `class` to `function`.
7+
- 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.
8+
- Note that `^16.0.0` is the last release supporting `NgModule`.
9+
- This is not semver compliant, I agree. But the direct usage of `PendingRequestsInterceptor` must be **very** low.
10+
311
## v16.0.0
412
- Added angular 18 support.
513

README.md

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -51,46 +51,29 @@ If you experience errors like below, **please double-check the version you use.*
5151

5252
`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 [...]`
5353

54-
## Requirements - HttpClientModule
54+
## Requirements - HttpClient
5555

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

6058
## Usage
6159

62-
From your Angular `AppModule`:
60+
From your Angular root component (`app.component.ts` for example):
6361

6462
```typescript
65-
import { BrowserModule } from '@angular/platform-browser';
66-
import { NgModule } from '@angular/core';
67-
// [...]
68-
import { AppComponent } from './app.component';
69-
import { HttpClientModule } from '@angular/common/http'; // <============
70-
import { NgHttpLoaderModule } from 'ng-http-loader'; // <============
71-
72-
@NgModule({
73-
declarations: [
74-
AppComponent
75-
],
76-
imports: [
77-
BrowserModule,
78-
HttpClientModule, // <============ (Perform HTTP requests with this module)
79-
NgHttpLoaderModule.forRoot(), // <============ Don't forget to call 'forRoot()'!
80-
],
81-
providers: [],
82-
bootstrap: [AppComponent]
63+
@Component({
64+
selector: 'app-root',
65+
standalone: true,
66+
templateUrl: './app.component.html',
67+
styleUrls: ['./app.component.scss'],
68+
imports: [NgHttpLoaderComponent] <====== import the component
8369
})
84-
export class AppModule { }
8570
```
8671

87-
In your app.component.html, simply add:
72+
In your `app.component.html`, simply add:
8873
```xml
8974
<ng-http-loader></ng-http-loader>
9075
```
91-
## Standalone components
92-
93-
If you prefer using standalone components, you should configure your `ApplicationConfig` like following:
76+
At last, configure your `ApplicationConfig` like following:
9477

9578
```typescript
9679
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
@@ -102,30 +85,11 @@ import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';
10285
export const appConfig: ApplicationConfig = {
10386
providers: [
10487
provideRouter(routes),
105-
provideHttpClient(
106-
withInterceptorsFromDi() // <== Don't forget to import the interceptors
107-
),
108-
importProvidersFrom(NgHttpLoaderModule.forRoot()) //<== Always call `forRoot`
88+
withInterceptors([pendingRequestsInterceptor$])
10989
],
11090
};
11191
```
112-
Then you can use `ng-http-loader` like this:
113-
```typescript
114-
import { Component } from '@angular/core';
115-
import {NgHttpLoaderModule} from "ng-http-loader";
11692

117-
@Component({
118-
selector: 'my-selector',
119-
standalone: true,
120-
imports: [
121-
NgHttpLoaderModule
122-
],
123-
template: `
124-
<ng-http-loader />`,
125-
})
126-
export class InlineComponent {
127-
}
128-
```
12993
## Customizing the spinner
13094

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

160124
@Component({
161125
selector: 'app-root',
126+
standalone: true,
162127
templateUrl: './app.component.html',
163128
styleUrls: ['./app.component.css'],
129+
imports: [NgHttpLoaderComponent]
164130
})
165131
export class AppComponent {
166132
public spinkit = Spinkit; // <============
@@ -188,8 +154,10 @@ import { AwesomeComponent } from 'my.awesome.component';
188154

189155
@Component({
190156
selector: 'app-root',
157+
standalone: true,
191158
templateUrl: './app.component.html',
192-
styleUrls: ['./app.component.css']
159+
styleUrls: ['./app.component.css'],
160+
imports: [NgHttpLoaderComponent]
193161
})
194162
export class AppComponent {
195163
public awesomeComponent = AwesomeComponent;

src/lib/components/ng-http-loader.component.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
import { Component, Input, OnInit, Type } from '@angular/core';
1111
import { merge, Observable, partition, timer } from 'rxjs';
1212
import { debounce, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
13-
import { PendingRequestsInterceptor } from '../services/pending-requests-interceptor.service';
1413
import { SpinnerVisibilityService } from '../services/spinner-visibility.service';
1514
import { Spinkit, SPINKIT_COMPONENTS } from '../spinkits';
1615
import { AsyncPipe, NgComponentOutlet, NgIf, NgStyle } from "@angular/common";
16+
import { PendingRequestsInterceptorConfigurer } from "../services/pending-requests-interceptor-configurer.service";
1717

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

44-
constructor(private pendingRequestsInterceptor: PendingRequestsInterceptor, private spinnerVisibility: SpinnerVisibilityService) {
44+
constructor(private pendingRequestsInterceptorConfigurer: PendingRequestsInterceptorConfigurer, private spinnerVisibility: SpinnerVisibilityService) {
4545
}
4646

4747
ngOnInit(): void {
@@ -51,10 +51,10 @@ export class NgHttpLoaderComponent implements OnInit {
5151
}
5252

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

5656
this.isVisible$ = merge(
57-
this.pendingRequestsInterceptor.pendingRequestsStatus$
57+
this.pendingRequestsInterceptorConfigurer.pendingRequestsStatus$
5858
.pipe(switchMap(() => showSpinner$.pipe(debounce(() => timer(this.debounceDelay))))),
5959
showSpinner$
6060
.pipe(switchMap(() => hideSpinner$.pipe(debounce(() => this.getVisibilityTimer$())))),
@@ -80,17 +80,17 @@ export class NgHttpLoaderComponent implements OnInit {
8080
private initFilteredUrlPatterns(): void {
8181
if (!!this.filteredUrlPatterns.length) {
8282
this.filteredUrlPatterns.forEach(e =>
83-
this.pendingRequestsInterceptor.filteredUrlPatterns.push(new RegExp(e))
83+
this.pendingRequestsInterceptorConfigurer.filteredUrlPatterns.push(new RegExp(e))
8484
);
8585
}
8686
}
8787

8888
private initFilteredMethods(): void {
89-
this.pendingRequestsInterceptor.filteredMethods = this.filteredMethods;
89+
this.pendingRequestsInterceptorConfigurer.filteredMethods = this.filteredMethods;
9090
}
9191

9292
private initFilteredHeaders(): void {
93-
this.pendingRequestsInterceptor.filteredHeaders = this.filteredHeaders;
93+
this.pendingRequestsInterceptorConfigurer.filteredHeaders = this.filteredHeaders;
9494
}
9595

9696
private updateExpirationDelay(showSpinner: boolean): void {

src/lib/ng-http-loader.module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
import { CommonModule } from '@angular/common';
1111
import { ModuleWithProviders, NgModule } from '@angular/core';
1212
import { NgHttpLoaderComponent } from './components/ng-http-loader.component';
13-
import { PendingRequestsInterceptorProvider } from './services/pending-requests-interceptor.service';
1413
import { SPINKIT_COMPONENTS } from './spinkits';
14+
import { pendingRequestsInterceptor$ } from "./services/pending-requests-interceptor";
15+
import { provideHttpClient, withInterceptors } from "@angular/common/http";
1516

1617
/**
1718
* @deprecated Will be removed in the next release, standalone component will become the default.
@@ -35,7 +36,7 @@ export class NgHttpLoaderModule {
3536
return {
3637
ngModule: NgHttpLoaderModule,
3738
providers: [
38-
PendingRequestsInterceptorProvider,
39+
provideHttpClient(withInterceptors([pendingRequestsInterceptor$])),
3940
]
4041
};
4142
}

src/lib/services/pending-requests-interceptor.service.ts renamed to src/lib/services/pending-requests-interceptor-configurer.service.ts

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@
77
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
88
*/
99

10-
import { HTTP_INTERCEPTORS, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
11-
import { ExistingProvider, Injectable } from '@angular/core';
10+
import { HttpRequest } from '@angular/common/http';
11+
import { Injectable } from '@angular/core';
1212
import { Observable, ReplaySubject } from 'rxjs';
13-
import { finalize } from 'rxjs/operators';
1413

1514
@Injectable({
1615
providedIn: 'root'
1716
})
18-
export class PendingRequestsInterceptor implements HttpInterceptor {
17+
export class PendingRequestsInterceptorConfigurer {
1918

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

30+
get pendingRequestsStatusSubject$(): ReplaySubject<boolean> {
31+
return this._pendingRequestsStatus$;
32+
}
33+
3134
get pendingRequests(): number {
3235
return this._pendingRequests;
3336
}
3437

38+
set pendingRequests(pendingRequests: number) {
39+
this._pendingRequests = pendingRequests;
40+
}
41+
3542
get filteredUrlPatterns(): RegExp[] {
3643
return this._filteredUrlPatterns;
3744
}
@@ -48,58 +55,28 @@ export class PendingRequestsInterceptor implements HttpInterceptor {
4855
this._forceByPass = value;
4956
}
5057

51-
private shouldBypassUrl(url: string): boolean {
58+
shouldBypassUrl(url: string): boolean {
5259
return this._filteredUrlPatterns.some(e => {
5360
return e.test(url);
5461
});
5562
}
5663

57-
private shouldBypassMethod(req: HttpRequest<unknown>): boolean {
64+
shouldBypassMethod(req: HttpRequest<unknown>): boolean {
5865
return this._filteredMethods.some(e => {
5966
return e.toUpperCase() === req.method.toUpperCase();
6067
});
6168
}
6269

63-
private shouldBypassHeader(req: HttpRequest<unknown>): boolean {
70+
shouldBypassHeader(req: HttpRequest<unknown>): boolean {
6471
return this._filteredHeaders.some(e => {
6572
return req.headers.has(e);
6673
});
6774
}
6875

69-
private shouldBypass(req: HttpRequest<unknown>): boolean {
76+
shouldBypass(req: HttpRequest<unknown>): boolean {
7077
return this._forceByPass
7178
|| this.shouldBypassUrl(req.urlWithParams)
7279
|| this.shouldBypassMethod(req)
7380
|| this.shouldBypassHeader(req);
7481
}
75-
76-
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
77-
const shouldBypass = this.shouldBypass(req);
78-
79-
if (!shouldBypass) {
80-
this._pendingRequests++;
81-
82-
if (1 === this._pendingRequests) {
83-
this._pendingRequestsStatus$.next(true);
84-
}
85-
}
86-
87-
return next.handle(req).pipe(
88-
finalize(() => {
89-
if (!shouldBypass) {
90-
this._pendingRequests--;
91-
92-
if (0 === this._pendingRequests) {
93-
this._pendingRequestsStatus$.next(false);
94-
}
95-
}
96-
})
97-
);
98-
}
9982
}
100-
101-
export const PendingRequestsInterceptorProvider: ExistingProvider[] = [{
102-
provide: HTTP_INTERCEPTORS,
103-
useExisting: PendingRequestsInterceptor,
104-
multi: true
105-
}];
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
4+
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
5+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
6+
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
7+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8+
*/
9+
10+
import { HttpEvent, HttpHandlerFn, HttpRequest } from '@angular/common/http';
11+
import { inject } from '@angular/core';
12+
import { Observable } from 'rxjs';
13+
import { finalize } from 'rxjs/operators';
14+
import { PendingRequestsInterceptorConfigurer } from "./pending-requests-interceptor-configurer.service";
15+
16+
export function pendingRequestsInterceptor$(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
17+
const pendingRequestsInterceptorConfigurer = inject(PendingRequestsInterceptorConfigurer);
18+
const shouldBypass = pendingRequestsInterceptorConfigurer.shouldBypass(req);
19+
20+
if (!shouldBypass) {
21+
pendingRequestsInterceptorConfigurer.pendingRequests++;
22+
23+
if (1 === pendingRequestsInterceptorConfigurer.pendingRequests) {
24+
pendingRequestsInterceptorConfigurer.pendingRequestsStatusSubject$.next(true);
25+
}
26+
}
27+
28+
return next(req).pipe(
29+
finalize(() => {
30+
if (!shouldBypass) {
31+
pendingRequestsInterceptorConfigurer.pendingRequests--;
32+
33+
if (0 === pendingRequestsInterceptorConfigurer.pendingRequests) {
34+
pendingRequestsInterceptorConfigurer.pendingRequestsStatusSubject$.next(false);
35+
}
36+
}
37+
})
38+
);
39+
}

src/lib/services/spinner-visibility.service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

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

1414
@Injectable({
1515
providedIn: 'root'
@@ -18,20 +18,20 @@ export class SpinnerVisibilityService {
1818

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

21-
constructor(private pendingRequestsInterceptor: PendingRequestsInterceptor) {
21+
constructor(private pendingRequestsInterceptorConfigurer: PendingRequestsInterceptorConfigurer) {
2222
}
2323

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

2828
show(): void {
29-
this.pendingRequestsInterceptor.forceByPass = true;
29+
this.pendingRequestsInterceptorConfigurer.forceByPass = true;
3030
this._visibility$.next(true);
3131
}
3232

3333
hide(): void {
3434
this._visibility$.next(false);
35-
this.pendingRequestsInterceptor.forceByPass = false;
35+
this.pendingRequestsInterceptorConfigurer.forceByPass = false;
3636
}
3737
}

src/public_api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export * from './lib/components/sk-wave/sk-wave.component';
1313
export * from './lib/components/ng-http-loader.component';
1414
export * from './lib/components/abstract.loader.directive';
1515

16-
export * from './lib/services/pending-requests-interceptor.service';
16+
export * from './lib/services/pending-requests-interceptor';
17+
export * from './lib/services/pending-requests-interceptor-configurer.service';
18+
1719
export * from './lib/services/spinner-visibility.service';
1820

1921
export * from './lib/ng-http-loader.module';

0 commit comments

Comments
 (0)