Progress Spinner in Angular: The Basics, Multiple Spinners, and Renaming During Runtime

About a month ago, ngx-spinner has been updated to support multiple spinners so that you can show a progress spinner in specific sections of your page instead of blocking your entire page. This feature comes in handy for those situations but if you have a dynamic control such as a list of panels dynamically created during runtime (perhaps, taken from the database) and you need a progress spinner for each, the current ngx-spinner does not allow you to rename your progress spinner during runtime (i.e. via TypeScript).

In this article, we first discuss the basic usage of the ngx-spinner, then, using multiple spinners on a non-dynamically created controls, and finally, take the case of dynamic controls.

Basic Usage

NOTE: Basic usage of the spinner can be found in the documentation here. The same discussion has been placed here for convenience.

Install the ngx-spinner using npm:

$ npm install ngx-spinner --save

Import the NgxSpinnerModule in the root module:

// Import library module
import { NgxSpinnerModule } from 'ngx-spinner';
 
@NgModule({
  imports: [
    // ...
    NgxSpinnerModule
  ]
})
export class AppModule { }

Place the ngx-spinner on your page:

<div>
  <router-outlet></router-outlet>
  <ngx-spinner type="timer" size="medium"></ngx-spinner>
</div>

You can configure how your spinner would look like in your html template by specifying the following parameters:

PropertyDescription
bdColorBackground color for the backdrop.
sizeSize of the spinner. [small, medium, large]
colorColor of the spinner.
typeSpinner animation type.
fullScreenTo enable/disable fullscreen mode.

Finally, use the spinner on your component:

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.scss']
})
export class HelloComponent implements OnInit {
  constructor(private spinnerService: NgxSpinnerService) { }

  foo(): void {
    this.spinnerService.show();
    this.dataService.getData()
      .pipe(finalize(() => this.spinnerService.hide()));
  }
}

Multiple Spinners

Now, lets take the case where you have several panels and you want to show different progress spinners for each:

<mat-card>
  <mat-card-header>
    <mat-card-title>Panel 1</mat-card-title>
  </mat-card-header>
  <mat-divider></mat-divider>
  <mat-card-content>
    <div>My data for Panel 1.</div>
  </mat-card-content>
  <ngx-spinner name="spinner-1" [fullScreen]="false" type="timer" size="medium"></ngx-spinner>
</mat-card>

<mat-card>
  <mat-card-header>
    <mat-card-title>Panel 2</mat-card-title>
  </mat-card-header>
  <mat-divider></mat-divider>
  <mat-card-content>
    <div>My data for Panel 2.</div>
  </mat-card-content>
  <ngx-spinner name="spinner-2" [fullScreen]="false" type="timer" size="medium"></ngx-spinner>
</mat-card>

<button (click)="showSpinner1">Show Spinner on Panel 1</button>
<button (click)="showSpinner2">Show Spinner on Panel 2</button>

<button (click)="hideSpinner1">Hide Spinner on Panel 1</button>
<button (click)="hideSpinner2">Hide Spinner on Panel 2</button>

To show or hide a specific spinner, you call the same show() or hide() methods, respectively, passing the name of the spinner:

showSpinner1() {
  this.spinnerService.show("spinner-1");
}

showSpinner2() {
  this.spinnerService.show("spinner-2");
}

hideSpinner1() {
  this.spinnerService.show("spinner-1");
}

hideSpinner2() {
  this.spinnerService.show("spinner-2");
}

Dynamic Controls

If you have a dynamic control (particularly, a repeater control such as multiple panels) that you created during run-time and you need to show the progress spinner individually on their respective sections, you may need to have a unique name on each of the progress spinners. Perhaps, you would do it something like:

<mat-card>
  <mat-card-header>
    <mat-card-title>{{panelTitle}}</mat-card-title>
  </mat-card-header>
  <mat-divider></mat-divider>
  <mat-card-content>
    <div>{{message}}</div>
  </mat-card-content>
  <ngx-spinner [attr.name]="'spinner-' + panelKey" [fullScreen]="false" type="timer" size="medium"></ngx-spinner>
</mat-card>

While you are able to assign the name dynamically here, this wouldn’t work as during the creation of the spinner, it will register an observable along with the current name of the spinner. The data fetch might have been invoked after the creation and thus, the observable would have already been created at that time.

What we need to do is that after data fetch, you have to unsubscribe the previously registered observable, then subscribe to a new observable with a new name. We need to have the instance of the spinner in your TypeScript component file to do that:

@ViewChild(NgxSpinnerComponent) panelSpinner: NgxSpinnerComponent;

ngAfterViewInit() {
  var spinnerName = "spinner-" + panelKey
  this.panelSpinner.spinner.name = spinnerName ;

  this.panelSpinner.ngUnsubscribe.next();
  this.panelSpinner.ngUnsubscribe.complete();

  this.spinnerService.getSpinner(spinnerName )
    .pipe(takeUntil(this.panelSpinner.ngUnsubscribe))
    .subscribe((spinner: any) => {
      this.panelSpinner.setDefaultOptions();
      Object.assign(this.panelSpinner.spinner, spinner);
      this.panelSpinner.show = spinner.show;
      if (this.panelSpinner.show) {
        this.panelSpinner.fullScreen = spinner.fullScreen;
        this.panelSpinner.onInputChange();
      }
    });
}

So, there you have it! You are now able to show the spinners on your dynamic control!

(I’ll cover creation of dynamic control on a future article.)