DEV Community

Discussion on: Loading indication in Angular

Collapse
 
johncarroll profile image
John Carroll

Incidentally I made an Angular library so solve this very task a while back: IsLoadingService.

In general you can simply hook it up in your root component and forget about it:

@Component({
  selector: 'app-root',
  template: `
    <mat-progress-bar
      *ngIf="(loadingService.isLoading$() | async)"
      mode="indeterminate"
      color="warn"
      style="position: absolute; top: 0; z-index: 100;"
    >
    </mat-progress-bar>

    <router-outlet></router-outlet>
  `,
})
export class AppComponent implements OnInit {
  constructor(public loadingService: IsLoadingService) {}
}

It will handle indicating loading during router navigation. So long as you use route resolvers and route guards, I find the majority of get related requests are covered.

Regarding mutations:

export class UserComponent  {
  constructor(
    private userService: UserService,
    public isLoadingService: IsLoadingService,
  ) {}

  create(name = "John Doe"): void {
    // loading turns off when the request completes
    this.isLoadingService.add(
      this.userService.create(new User(name)).subscribe()
    )
  }
}

Or sometimes I'll have a persistent observable connection, and I only want to indicate loading while it is fetching the first batch of data but then after that hide any loading from the user. In this case you can just pass an observable to IsLoadingService#add() and it will take(1) from the observable and then pass the observable back to the caller. I.e.

@Component({
  template: `
    <div *ngIf="isLoadingService.isLoading$() | async">
      <!-- this will only display during initial loading -->
      Creating, please wait <loading-indicator></loading-indicator>
    </div>

    <div *ngFor='let user of users$ | async'>
      <!-- do stuff -->
    </div>
  `
})
export class UserComponent  {
  users$: Observable<User>;

  constructor(
    private userService: UserService,
    public isLoadingService: IsLoadingService,
    private angularFire: AngularFirestore,
  ) {}

  ngOnInit() {
    // here we let the `async` pipe take care of unsubscribe for us
    this.users$ =
      this.isLoadingService.add(
        this.angularFire.collection(usersCollection).valueChanges(),
      );
  }
}
Collapse
 
n_mehlhorn profile image
Nils Mehlhorn

Interesting, but that would give you global indication again, right? You couldn't realiably use it for the 'Creating, please wait'-part as some other request might also cause the loading.

Another suggestion: it'd probably be good to get rid of the function call in the template loadingService.isLoading$(). Its usually called everytime change detection runs. As you've already got observables there, it shouldn't be that hard - you could probably transfer the method arguments into something that's operator based.

Collapse
 
johncarroll profile image
John Carroll • Edited

Ya I noticed my original comment was getting really long so I cut short the details, but you can call IsLoadingService#add() with an optional key argument which allows you to trigger separate loading indicators (though in practice, I've only ever used this option once or twice).

isLoadingService.add(subscription, {key: 'my-key'})

isLoadingService.isLoading$({key: 'my-key'})

Regarding the function call in the template, I thought about that but, in testing, decided removing it was a solution in search of a problem. The function is merely returning a cached value and, even running every change detection cycle, is very performant. If it is ever shown to be a problem I'll certainly address it, but, at the moment, I'm skeptical that any change would be an improvement.

Edit
I could imagine the function call might be a problem if you were rendering hundreds (maybe dozens) of loading indicators on the screen at once, but I think this scenario would only occur if you were using dynamic key values to, for example, show which rows in a spreadsheet were loading. This scenario is already not supported.