TL;DR: A Service for hooking into the xhr events of all http calls. Use it to make spinners spin, turkeys dance or whatever floats your boat while your app fetches data and the user needs to know.
So I started writing a feedback service for angular“™s http calls. The idea is simple. When my app does http calls, I want my spinner to spin!
Now you don“™t want to constantly set a „˜loading“™ flag in your components somewhere or manually show loading overlays all the time right? All you want is a small spinner in the top right that spins on activity. Maybe you also want a toast or some notification to be shown on completed transactions.
I started building this and wrote about it in this blog post, so if you want to read the full story, read that one first.
Version 2: Pulling the UI feedback code out of the API Service
In my previous post, you had to call the vFeedbackService from somewhere when you did an http call and pass it the observable. That was ugly! Sure you could get spinners and toasts in 2 lines of code, but you want 0 lines of code in your components! Just a spinner, is that too much to ask?
So new plan:
- Hook into the code that angular uses to make HTTP calls. Grab those events!
- Create an observable based on those events. Use it to notify spinner components of stuff going on
- Write a spinner component that hooks into this and spins every time when HTTP calls are going on. All other code is untouched
Digging through the Angular code
If you don“™t care about this, just skip below to the solution.
Digging into the http code of Angular 2, it becomes clear how it works and what to do. Http uses a ConnectionBackend which is used to create a connection. This connection is created using BrowserXhr.build() (in the browser). So if we wrap this, returning the original object (XMLHttpRequest) but hooking into its events, we“™re good. We get all http requests and their open, load, error, abort and progress events.
Creating the new Service to subscribe to
To hook into the events, we override the build() method of BrowserXhr to ensure we grab the events before handing back the original object.
[code language=“javascript“]
//imports …
export class CustomBrowserXhr extends BrowserXhr {
private _observable: Observable;
private _subscriber: Subscriber;
constructor() {
super();
this._observable = Observable.create(subscriber => {
this._subscriber = subscriber;
}).share();
}
get observable(): Observable {
return this._observable;
}
build(): any {
let xhr = super.build();
if (!this._subscriber) return xhr;
//at the beginning, we create an event that notifies an opening of a connection
this._subscriber.next({type: ‚open‘, event: {}});
xhr.onprogress = (event) => {
this._subscriber.next({type: ‚progress‘, event: event});
};
xhr.onload = (event) => {
this._subscriber.next({type: ‚load‘, event: event});
};
xhr.onerror = (event) => {
this._subscriber.next({type: ‚error‘, event: event});
};
xhr.onabort = (event) => {
this._subscriber.next({type: ‚abort‘, event: event});
};
return xhr;
}
}
[/code]
Now, we have something to subscribe to.
To ensure this is actually used, we of course also need to override the default BrowserXhr
[code language=“javascript“]
@NgModule({
declarations: [
AppComponent
],
imports: [
//…
CustomBrowserXhr // <<<
],
providers: [{provide: BrowserXhr, useExisting: CustomBrowserXhr}], //!!! <<<
bootstrap: [AppComponent]
})
[/code]
Now, we have successfully wrapped an angular internal injectable with our code!
Let“™s build a spinner (I am using angular material) that shows on ongoing http events and hides when they are all completed/aborted/cancelled or otherwise closed
[code language=“javascript“]
@Component({
selector: ’steak-working-spinner‘,
template: „,
styleUrls: [‚working-spinner.component.scss‘]
})
export class WorkingSpinnerComponent implements OnInit, OnDestroy {
private _visible: boolean;
private _subscription: AnonymousSubscription;
private _connectionCounter = 0;
constructor(public browserXhr: CustomBrowserXhr) {
}
ngOnInit() {
this._subscription = this.browserXhr.observable.subscribe(next => {
console.log(next.type + ‚ next received in spinner‘);
switch (next.type) {
case ‚open‘:
this._connectionCounter++;
break;
case ‚load‘:
this._connectionCounter–;
break;
case ‚abort‘:
this._connectionCounter–;
break;
case ‚error‘:
this._connectionCounter–;
break;
}
this._visible = this._connectionCounter > 0; //if larger 0, its visible! otherwise hide us
});
}
ngOnDestroy(): void {
if (!this._subscription) return;
this._subscription.unsubscribe();
this._subscription = null;
}
}
[/code]
This can now be placed anywhere in the app, as often as you want and whereever a process should be indicated. It can also be any UI, all it needs is react to the events of the http calls.
The code above also ensures, the spinner only stops spinning on all http calls being finished. This is helpful, if users quickly perform calls or if they click once but this triggers 2+ http calls.
The resulting UI
If you like this, maybe you can help: I have built this and would like to share it. But I am unsure how to „˜correctly“™ publish it as an npm module for others to just import and use.
1 Kommentar
Pingback: Building a visual feedback service for angular http calls with material | The Cattle Crew Blog