Angular: How to easily display loading indicators
Tags: angular, ux, githunt
Published: 2019-07-02 21:16:30

A common task for Angular applications is showing a loading indicator during page navigation, or showing a loading indicator while POSTing data to a server, or showing a loading indicator while `*something*` is happening.

I recently [read a great post by Nils Mehlhorn](https://dev.to/angular/loading-indication-in-angular-52b6) talking about various strategies that you can use to display/manage loading indicators. It's a great read, but I don't think any of the options discussed are quite as nice as using the small [IsLoadingService](https://gitlab.com/service-work/is-loading) I made for this task. Allow me to demonstrate using some examples which build off each other (some of the examples are taken from Nils' post).

## Indicating that a page is loading

Let's start with page loading. Many (most?) Angular application's will make use of the Angular Router to allow navigation between sections of an app. We want to display a loading indicator while navigation is pending.

For our loading indicator example, we will use the [MatProgressBar component](https://material.angular.io/components/progress-bar/overview#indeterminate) from the `@angular/material` library, and display it along the top of the page.

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

    <router-outlet></router-outlet>
  `,
})
export class AppComponent {
  isLoading: Observable<boolean>;

  constructor(
    private isLoadingService: IsLoadingService,
    private router: Router,
  ) {}

  ngOnInit() {
    this.isLoading = this.isLoadingService.isLoading$();

    this.router.events
      .pipe(
        filter(
          event =>
            event instanceof NavigationStart ||
            event instanceof NavigationEnd ||
            event instanceof NavigationCancel ||
            event instanceof NavigationError,
        ),
      )
      .subscribe(event => {
        // If it's the start of navigation, `add()` a loading indicator
        if (event instanceof NavigationStart) {
          this.isLoadingService.add();
          return;
        }

        // Else navigation has ended, so `remove()` a loading indicator
        this.isLoadingService.remove();
      });
  }
}
```

In this example, we use the `IsLoadingService` to get our loading state as an observable, save it in the class property `isLoading`, and subscribe to it in our root component's template. When loading is `false`, the loading indicator will disappear.

The [Angular IsLoadingService](https://gitlab.com/service-work/is-loading) is a small (less than 1kb minzipped) service for angular that helps us track whether "something" is loading. We can subscribe to loading state via `IsLoadingService#isLoading$()` which returns an `Observable<boolean>` value indicating if the state is loading or not.

- To indicate that something has started loading, we can call `IsLoadingService#add()`.
- When something has stopped loading, we can call `IsLoadingService#remove()`. 
- IsLoadingService will emit `true` from `isLoading$()` so long as there are one or more things that are still loading.

Because we want to display a loading bar during router navigation, we subscribe to angular `Router` events. When a `NavigationStart` event is emitted, we say that loading has started (via `add()`). When `NavigationEnd || NavigationCancel ||NavigationError` event is emitted, we say that loading has stopped (via `remove()`).

With this code, our app will now display a loading bar at the top of the page while navigating between routes.

- Note: if we called `IsLoadingService#add()` with a `Subscription`, `Promise`, or `Observable` value, we wouldn't need to call `remove()`.
  - When passed a `Subscription` or `Promise`, `add()` will automatically mark that the `Subcription` or `Promise` has stopped loading when it completes.
  - In the case of an `Observable` value, `add()` will `take(1)` from the observable and subscribe to it, noting that loading for that `Observable` has stopped when it emits it's next value.
  - More on this below...

## Waiting for data

Having completed the task of adding a "page loading" indicator during router navigation, now we want to display a loading indicator when we asynchronously fetch a list of users from a service. We decide we want to trigger the same progress bar indicator that we just set up to indicate that a page is loading.

```ts
@Component({
  selector: 'user-component'
  template: `
    <list-component>
      <profile-component *ngFor="let user of users | async" [user]='user'>
      </profile-component>
    </list-component>
  `
})
export class UserComponent implements OnInit  {
  users: Observable<User[]>;

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

  ngOnInit() {
    this.users =
      this.isLoadingService.add( this.userService.getAll() );
  }
}
```

_Note how, in this example, there are no additional subscriptions or variables we need to manage._

When you call `IsLoadingService#add()` with an `Observable` (or `Promise` or `Subscription`) argument, that argument is returned.

So this code...

```ts
this.users = this.isLoadingService.add(this.userService.getAll());
```

Is the same as this code, so far as `this.users` is concerned.

```ts
this.users = this.userService.getAll();
```

Seperately (as mentioned before), when `IsLoadingService#add()` is passed an `Observable` value, IsLoadingService will `take(1)` from the value and subscribe to the results. When this component is initialized, `this.isLoadingService.add( this.userService.getAll() )` will be called triggering the "page loading" indicator we set up previously. When the observable returned from `this.userService.getAll()` emits for the first time, IsLoadingService will know that this observable has stopped loading and update the "page loading" indicator as appropriate.

If `this.userService.getAll()` returned a promise (or subscription), we could also pass it to `this.isLoadingService.add()` and achieve similar results.

## Submitting a form

Next up, we want to let our users create a new `User` by submitting a user creation form. When we do this, we'd like to disable the form's "submit" button and style it to indicate that the form is pending. We also decide that, in this case, we _do not_ want to display the same "page loading" progress bar as before.

One way of accomplishing this task, is the following...

```ts
@Component({
  selector: 'user-component'
  template: `
    <form [formGroup]='userForm' novalidate>
      <mat-form-field>
        <mat-label> Your name </mat-label>
        <input matInput formControlName='name' required />
      </mat-form-field>

      <button
        mat-button
        color='primary'
        [disabled]='userFormIsPending | async'
        (click)='submit()'
      >
        Submit
      </button>
    </form>
  `,
  styles: [`
    .mat-button[disabled] {
      // button pending styles...
    }
  `]
})
export class UserComponent implements OnInit  {
  userForm: FormGroup;
  userFormIsPending: Observable<boolean>;

  constructor(
    private userService: UserService,
    private isLoadingService: IsLoadingService,
    private fb: FormBuilder,
    private router: Router,
  ) {}

  ngOnInit() {
    this.userForm = this.fb.group({
      name: ['', Validators.required],
    });

    this.userFormIsPending =
      this.isLoadingService.isLoading$({ key: 'create-user' });
  }

  async submit() {
    if (this.userForm.invalid) return;

    const response = await this.isLoadingService.add(
      this.usersService.createUser(
        this.userForm.value
      ).toPromise(),
      { key: 'create-user' }
    )

    if (response.success) {
      this.router.navigate(['user', response.data.userId, 'profile']);
    }
  }
}
```

There are some new concepts here. For one, instead of subscribing to "default" loading state, we're subscribing to the "create-user" loading state by passing the `key` option to `IsLoadingService#isLoading$()`. In this case, `userFormIsPending` will ignore the page loading indicator state. It only cares about loading things added using `add({key: 'create-user'})`.

Next, when our `userForm` is submitted, we pass the form value to `UsersService#createUser()` which returns an observable. We transform that observable into a `Promise`, and `await` the result. We also pass this promise to `IsLoadingService#add()` with the `key: 'create-user'`.

If our mutation is a success, we navigate to the new user's profile page.

In the component template, we subscribe to `userFormIsPending` and, when a `submit()` is pending, the "submit" button is automatically disabled.

This example is pretty clean, but it still has this otherwise unnecessary `userFormIsPending` property. We can improve things...

### Simplifying form submission with [IsLoadingPipe](https://gitlab.com/service-work/is-loading#isloadingpipe)

A quick way we can simplify the previous example is using the optional `IsLoadingPipe` alongside `IsLoadingService`. The `IsLoadingPipe` simplifies the task of subscribing to loading state inside a component's template.

The IsLoadingPipe is an angular pipe which recieves a `key` argument and returns an `isLoading$({key})` observable for that key. Importantly, it plays nice with Angular's change detection. Because the IsLoadingPipe returns an observable, you should use it with the Anguilar built-in `AsyncPipe`.

```ts
@Component({
  selector: 'user-component'
  template: `
    <form [formGroup]='userForm' novalidate>
      <mat-form-field>
        <mat-label> Your name </mat-label>
        <input matInput formControlName='name' required />
      </mat-form-field>

      <button
        mat-button
        color='primary'
        [disabled]='"create-user" | swIsLoading | async' <!-- change is here -->
        (click)='submit()'
      >
        Submit
      </button>
    </form>
  `,
  styles: [`
    .mat-button[disabled] {
      // button pending styles...
    }
  `]
})
export class UserComponent implements OnInit  {
  userForm: FormGroup;

  constructor(
    private userService: UserService,
    private isLoadingService: IsLoadingService,
    private fb: FormBuilder,
    private router: Router,
  ) {}

  ngOnInit() {
    this.userForm = this.fb.group({
      name: ['', Validators.required],
    });
  }

  async submit() {
    if (this.userForm.invalid) return;

    const response = await this.isLoadingService.add(
      this.usersService.createUser(
        this.userForm.value
      ).toPromise(),
      { key: 'create-user' }
    )

    if (response.success) {
      this.router.navigate(['user', response.data.userId, 'profile']);
    }
  }
}
```

_Notice, we no longer need to have the `userFormIsPending` property on our component._

However, we can improve this example further. In addition to simply disabling the userForm "submit" button, we'd like to add an "is-loading" css class to the button during loading. We'd also like to add an animated css spinner to the button while it is loading.

### Improving form submission with [IsLoadingDirective](https://gitlab.com/service-work/is-loading#isloadingdirective)

Using the optional `IsLoadingDirective` alongside `IsLoadingService`, we can improve our previous example. The `IsLoadingDirective` allows us to easily style (and optionally disable) elements based on loading state.

```ts
@Component({
  selector: 'user-component'
  template: `
    <form [formGroup]='userForm' novalidate>
      <mat-form-field>
        <mat-label> Your name </mat-label>
        <input matInput formControlName='name' required />
      </mat-form-field>

      <button
        mat-button
        color='primary'
        swIsLoading='create-user' <!-- change is here -->
        (click)='submit()'
      >
        Submit
      </button>
    </form>
  `,
  styles: [`
    .sw-is-loading {
      // button styles...
    }

    .sw-is-loading .sw-is-loading-spinner {
      // spinner styles
    }
  `]
})
export class UserComponent implements OnInit  {
  userForm: FormGroup;

  constructor(
    private userService: UserService,
    private isLoadingService: IsLoadingService,
    private fb: FormBuilder,
    private router: Router,
  ) {}

  ngOnInit() {
    this.userForm = this.fb.group({
      name: ['', Validators.required],
    });
  }

  async submit() {
    if (this.userForm.invalid) return;

    const response = await this.isLoadingService.add(
      this.usersService.createUser(
        this.userForm.value
      ).toPromise(),
      { key: 'create-user' }
    )

    if (response.success) {
      this.router.navigate(['user', response.data.userId, 'profile']);
    }
  }
}
```

In this example, we apply the `IsLoadingDirective` to our "submit" button via `swIsLoading='create-user'`. We pass the `'create-user'` key to the directive, telling it to subscribe to "create-user" loading state.

When the "create-user" state is loading, the IsLoadingDirective will automatically disable the "submit" button and add a `sw-is-loading` css class to the button. When loading stops, the button will be enabled and the `sw-is-loading` css class will be removed.

Because this is a button/anchor element, the IsLoadingDirective will also add a `sw-is-loading-spinner` child element to the "submit" button.

```html
<sw-is-loading-spinner class="sw-is-loading-spinner"></sw-is-loading-spinner>
```

Using css, you can add a spinner animation ([example here](https://tobiasahlin.com/spinkit/)) to this element so that the button displays a pending animation during loading. All of these options are configurable. For example, if you don't want a spinner element added to buttons when they are loading, you can configure that globally by injecting new defaults or locally via `swIsLoadingSpinner=false`.

### Triggering multiple loading indicators at once

As one last variation on the above, say we wanted to trigger _both_ the page loading indicator as well as the "create-user" indicator during form submission. You can accomplish this by passing an array of keys to `IsLoadingService#add()`.

In this case, you could update the previous example with...

```ts
export class UserComponent implements OnInit {
  async submit() {
    if (this.userForm.invalid) return;

    const response = await this.isLoadingService.add(
      this.usersService.createUser(this.userForm.value).toPromise(),
      { key: ['default', 'create-user'] }, // <-- change here
    );

    if (response.success) {
      this.router.navigate(['user', response.data.userId, 'profile']);
    }
  }
}
```

Calling `IsLoadingService#isLoading$()` without a key argument is the same as calling it with `isLoading$({key: 'default'})`, so you can trigger the "default" loading state by passing `"default"` as a key.

## Conclusion

IsLoadingService has been created iteratively over the past year or so. At this point, I find that it greatly simplifies managing loading indicators in my apps.

### You can check it out at: https://gitlab.com/service-work/is-loading