Spinners are ubiquitous, and most often, ubiquitous at an application level - they’re used everywhere. It should come as no surprise that, in the course of working on an application, I had a requirement to display a spinner while a particular service finished calling to a server and a response was received. The application users had to be prevented from interacting with elements on the page while the spinner was displayed.

Luckily, I happened across a super cool post by Muhammed Hassan on the very subject: ANGULAR2: USING CUSTOM LOADER / SPINNER AS SERVICE IN ANGULAR 2 APPLICATION.

Essentially, what you do is this:

  1. Add some CSS for a spinner (in this case, CSS Overlay Spinner by MattIn4D) into your application.

    Stick it in the app.component.scss file, as the class will be used in app.component.html template file, specifically in the router outlet so it can be used throughout the application, but we’ll get to that shortly.

    .tootlbar-icon {
    padding: 0 14px;
    }
    
    .tootlbar-spacer {
    flex: 1 1 auto;
    }
    
    /* Absolute Center Spinner */
    .loading {
    position: fixed;
    z-index: 999;
    height: 2em;
    width: 2em;
    overflow: show;
    margin: auto;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    }
    
    /* Transparent Overlay */
    .loading:before {
    content: '';
    display: block;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0,0,0,0.3);
    }
    
    /* :not(:required) hides these rules from IE9 and below */
    .loading:not(:required) {
    /* hide "loading..." text */
    font: 0/0 a;
    color: transparent;
    text-shadow: none;
    background-color: transparent;
    border: 0;
    }
    
    .loading:not(:required):after {
    content: '';
    display: block;
    font-size: 10px;
    width: 1em;
    height: 1em;
    margin-top: -0.5em;
    -webkit-animation: spinner 1500ms infinite linear;
    -moz-animation: spinner 1500ms infinite linear;
    -ms-animation: spinner 1500ms infinite linear;
    -o-animation: spinner 1500ms infinite linear;
    animation: spinner 1500ms infinite linear;
    border-radius: 0.5em;
    -webkit-box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.5) -1.5em 0 0 0, rgba(0, 0, 0, 0.5) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0;
    box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) -1.5em 0 0 0, rgba(0, 0, 0, 0.75) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0;
    }
    
    /* Animation */
    
    @-webkit-keyframes spinner {
        0% {
            -webkit-transform: rotate(0deg);
            -moz-transform: rotate(0deg);
            -ms-transform: rotate(0deg);
            -o-transform: rotate(0deg);
            transform: rotate(0deg);
        }
        100% {
            -webkit-transform: rotate(360deg);
            -moz-transform: rotate(360deg);
            -ms-transform: rotate(360deg);
            -o-transform: rotate(360deg);
            transform: rotate(360deg);
        }
    }
    @-moz-keyframes spinner {
        0% {
            -webkit-transform: rotate(0deg);
            -moz-transform: rotate(0deg);
            -ms-transform: rotate(0deg);
            -o-transform: rotate(0deg);
            transform: rotate(0deg);
        }
        100% {
            -webkit-transform: rotate(360deg);
            -moz-transform: rotate(360deg);
            -ms-transform: rotate(360deg);
            -o-transform: rotate(360deg);
            transform: rotate(360deg);
        }
    }
    @-o-keyframes spinner {
        0% {
            -webkit-transform: rotate(0deg);
            -moz-transform: rotate(0deg);
            -ms-transform: rotate(0deg);
            -o-transform: rotate(0deg);
            transform: rotate(0deg);
        }
        100% {
            -webkit-transform: rotate(360deg);
            -moz-transform: rotate(360deg);
            -ms-transform: rotate(360deg);
            -o-transform: rotate(360deg);
            transform: rotate(360deg);
        }
    }
    @keyframes spinner {
        0% {
            -webkit-transform: rotate(0deg);
            -moz-transform: rotate(0deg);
            -ms-transform: rotate(0deg);
            -o-transform: rotate(0deg);
            transform: rotate(0deg);
        }
        100% {
            -webkit-transform: rotate(360deg);
            -moz-transform: rotate(360deg);
            -ms-transform: rotate(360deg);
            -o-transform: rotate(360deg);
            transform: rotate(360deg);
        }
    }
    
  2. Create a new Angular spinner service

    Effectively, all this service will be doing is just relaying that a component would like to communicate that the spinner should be, well, spinning. The app.component.ts will subscribe to those changes in state, and turn the spinner on or off with a simple boolean.

    import { Injectable } from '@angular/core';
    import { BehaviorSubject } from 'rxjs/BehaviorSubject';
    
    @Injectable({
        providedIn: 'root',
    })
    export class SpinnerService {
        public spinnerStatus: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
    
        displaySpinner(value: boolean) {
            this.spinnerStatus.next(value);
        }
    }
    
  3. Register the new spinner service in app.module.ts

    // imports
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpClientModule } from '@angular/common/http';
    
    import { AppComponent } from './app.component';
    import { ItemDirective } from './item.directive';
    
    
    // @NgModule decorator with its metadata
    @NgModule({
    declarations: [
        AppComponent,
        ItemDirective
    ],
    imports: [
        BrowserModule,
        FormsModule,
        HttpClientModule
    ],
    providers: [SpinnerService],
    bootstrap: [AppComponent]
    })
    export class AppModule { }
    
  4. Stick the spinner tag within the router outlet in your app.component.html

    Add an ngIf in the span. If the ngIf evaluates to true, then the “loading” css class will be applied, and we’ll see our sweet spinner in action.

    <router-outlet>
        <span *ngIf="showSpinner" class="loading"></span>
    </router-outlet>
    
  5. Inject the spinner service into the app.component.ts

    We want to be able to set whether we should trigger showing the spinner in the router outlet. In order to do that, we need to inject the spinner service into our app.component.ts, and add that boolean trigger, showSpinner. When any component in the app calls displaySpinner and passes in true, the spinner will display. Likewise, a false will shut it off as the ngIf will evaluate to false.

    Easy peasey.

    import { Component, OnInit } from '@angular/core';
    
    import { LoaderService } from './shared/service/loader.service';
    
    @Component({
        selector: 'my-app',
        templateUrl: './app/app.component.html',
        styleUrls: ['./app/app.component.css']
    })
    
    export class AppComponent implements OnInit {
        showSpinner: boolean;
    
        constructor(
            private spinnerService: SpinnerService) {
        }
    
        ngOnInit() {
            this.spinnerService.displaySpinner.subscribe((showSpinner: boolean) => {
                this.showSpinner = showSpinner;
            });
        }
    }
    
  6. Trigger the spinner by injecting the service into a component in your app, and calling showSpinner on it

    Any component will do. ‘true’ shows the spinner, ‘false’ turns it off. Try this sucker out!

    import { Component, OnInit } from '@angular/core';
    import { LoaderService } from '../loader.service';
    
    @Component({
        selector: 'app-any',
        templateUrl: '../any.component.html'
    })
    
    export class AnyComponent implements OnInit {
    
        constructor(
            private spinnerService: SpinnerService,
            private longRequestService: LongRequestService) {
        }
    
        ngOnInit() {
            this.loaderService.display(true);
            this.longRequestService.getSomeLongRunningThing().then({
                this.loaderService.display(false);
            });
        }
    }
    


    That’s it for today!

    Now, go code and meditate.

    It’s good for you :)