Show loading indicator in Angular while waiting for a RxJS observable
Asked Answered
A

4

9

I have an Angular component that listens for a route parameter for a user id to change and when it does it loads the user details. The user details takes a few seconds to return data from the API.

If i'm viewing details for User A and then click to view details on User B, it continues to show User A until User B details are returned a few seconds after my click. Is there a way I can show a loading indicator or just blank it out while it's retrieving data for User B?

User details component:

ngOnInit(): void {
  this.userDetails = this.route.paramMap.pipe(
    switchMap((params: ParamMap) => this.userService.getUserDetails(+params.get('userId')))
  );
}

User details template:

<div *ngIf="userDetails | async as userDetails">
  <h1>{{userDetails.firstName}} {{userDetails.lastName}}</h1>
</div>

I would like the user details div to either be blank or show some sort of loading indicator if that inner switchMap is currently running. I know one option would be to have a loading variable that I set to true before the switchMap and false after the switchMap and use that in the *ngIf of the div, but I'm hoping there was a slicker way to not have to have loading variables for EVERY one of these situations.

I have an example StackBlitz: https://stackblitz.com/edit/angular-ng-busy-yxo1gu

The goal is when I click the User B button, User A information should disappear while User B is loading. I have dozens of this scenario in my app so I'm looking for the cleanest way to do this.

Ause answered 13/12, 2018 at 18:1 Comment(0)
F
9

If you just want the content to disappear you can emit for example null when you start fetching a different user:

this.userDetails = this.userId.asObservable().pipe(
  switchMap(id => merge(
    of(null),
    this.getUserDetails(id)
  )),
);

The merge Observable creation method will reemit emit null and then wait until getUserDetails completes. You don't even need ng-busy for this.

Your updated demo: https://stackblitz.com/edit/angular-ng-busy-vwteyf?file=src/app/app.component.ts

Fomentation answered 13/12, 2018 at 21:6 Comment(0)
K
10

you can change your *ngIf

ngIf="userDetails | async as userDetails else #loading"

then

<div #loading>
  loading...
</div>

Reference

Kreegar answered 13/12, 2018 at 18:11 Comment(4)
This works for loading the first user. But when I click on a different user it continues to show the first user's information until the new user has finished loading since the component is reused.Ause
if you assign this.userDetails = null before you start async operation, it will be undefined that moment, so it falls to else blockLalonde
I get this error in my template: Parser Error: Unexpected token # at column 76Algonquian
@FlorianLeitgeb this might help beginners, so I decided to add this comment, Angular ngIf directive else block only expects the template name, so you cannot place # here with the template name. it should be ngIf="userDetails | async as userDetails else loading" , Also I think you should use <ng-template #loading>...</ng-template> to setup your loading block in html.Atalanti
F
9

If you just want the content to disappear you can emit for example null when you start fetching a different user:

this.userDetails = this.userId.asObservable().pipe(
  switchMap(id => merge(
    of(null),
    this.getUserDetails(id)
  )),
);

The merge Observable creation method will reemit emit null and then wait until getUserDetails completes. You don't even need ng-busy for this.

Your updated demo: https://stackblitz.com/edit/angular-ng-busy-vwteyf?file=src/app/app.component.ts

Fomentation answered 13/12, 2018 at 21:6 Comment(0)
B
2

You can use ng-busy (or basically any other npm component out there) for displaying a loading indicator in each http call (promise or observable in your case) in your app.

EDIT: Regarding the fact that you're using Observable, you can use .toPromise in order to work with ng-Busy. Modified the DEMO to show you how to do that.

Here is a Minimal, Complete, and Verifiable example of ng-busy with your code.

Bethannbethanne answered 13/12, 2018 at 18:15 Comment(2)
That's pretty close, but my userDetails is an observable. I took your stackblitz and modified it closer to what I have with userDetails. I have dozens of this scenario throughout my app so I'm looking for the slickest way to remove User A details after the user clicks on the User B button. stackblitz.com/edit/angular-ng-busy-yxo1guAuse
@Ause did you see the updated DEMO? did it help you?Bethannbethanne
I
2

Try my own library ngrx-busy to display loading indicator with any observables. Unlike the ng-busy library you don't need to cast it to Promises or store Subscription. Also you don't need to care about change detection strategy.

Just register the library single time (add Http interceptor and NgrxModule) and use it like this:

<ngrx-busy>
  <div *ngIf="userDetails | async as userDetails">
    <h1>{{userDetails.firstName}} {{userDetails.lastName}}</h1>
  </div>
</ngrx-busy>
@ViewChild(NgrxBusy) busy: NgrxBusy;

ngOnInit(): void {
  this.userDetails = this.route.paramMap.pipe(
    switchMap((params: ParamMap) => this.userService.getUserDetails(+params.get('userId'))),
    withBusy(() => this.busy)
  );
}

ngrx-busy

Ier answered 28/1, 2022 at 21:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.