Angular 8 Reusable Loader/Spinner Component

I am a very impatient person. In fact, everyone is when it comes to interacting with a user interface. Everything needs to be snappy and visually intuitive. When a user clicks on something or if there’s anything happening in the application, they expect a visual indicator. If there’s no acknowledgment, you are all left to wonder if the button you just clicked did run something successfully or it crashed. This can be very irritating and that is why we need a spinner/loader.

It didn’t compel me to write my own spinner component at first. After all, why should one reinvent the wheel? Disappointment soon knocked my doors as I couldn’t find any library that would fulfill my needs. I needed a reusable spinner component that I could use inside any HTML element and would overlay the original contents of that element.

After some research, I decided to use a spinner element made purely with CSS. Here, we are using this awesome loader/spinner library called load-awesome. It requires no JavaScript for animation, is completely CSS (no images) and the spinners are perfect on retina displays. As of writing, it has about 53 different animations, all of them really cool.

Install load-awesome

npm install load-awesome --save

After installing load-awesome, we need to add spinner’s style in angular.json file. There are plenty of styles to choose from. For now, let’s use line-spin-fade-rotating.

"styles": [
  "node_modules/load-awesome/css/line-spin-fade-rotating.css",
  "src/styles.scss"
]

Let’s start creating our spinner component.

import {Component, OnInit} from '@angular/core';

@Component({
  selector: 'app-spinner',
  templateUrl: './spinner.component.html',
  styleUrls: ['./spinner.component.scss']
})
export class SpinnerComponent implements OnInit {

  constructor() {
  }

  message: string;

  ngOnInit() {

  }
}

SpinnerComponent is where we will have our spinner icon and the backdrop rendered.

<div class="spinner-container">
  <div class="spinner-content">
    <div class="spinner-icon-container">
      <div class="la-line-spin-fade-rotating la-2x spinner-icon">
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
      </div>
    </div>
    <div *ngIf="message" class="spinner-message">{{message}}</div>
  </div>
</div>

As you can see here, we are using la-line-spin-fade-rotating class for the css spinner element. We also need to add enough empty div elements for the spinner to animate properly. The number of empty divisions required might be different based on the CSS spinner class being used.

Next is adding some styles to the component.

.spinner-container {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  height: 100%;
  width: 100%;
  background-color: rgba(0, 0, 0, .8);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10000;
}

.spinner-icon-container {
  display: flex;
  justify-content: center;
}

.spinner-icon {
  display: block;
}

.spinner-message {
  margin-top: 10px;
  display: block;
  color: white;
  font-size: 14px;
  font-weight: bold;
}

The container for the spinner component needs to be absolute position. This is because we want it to be positioned above the existing content. You may add/change the styles as per your need.

We now need an Angular Directive to help us with adding our spinner component to the host element. We’ll be writing a structural directive that will let us change the DOM element.

import {
  ComponentFactory,
  ComponentFactoryResolver,
  ComponentRef,
  Directive,
  HostListener,
  Input,
  OnInit,
  TemplateRef,
  ViewContainerRef
} from '@angular/core';
import {SpinnerComponent} from '../components/spinner/spinner.component';

@Directive({
  selector: '[appSpinner]'
})
export class AppSpinnerDirective implements OnInit {

  private message: string;

  @Input('appSpinner')
  set showSpinner(spinning: boolean) {
    this.container.clear();

    if (spinning) {
      this.container.createEmbeddedView(this.template);
      this.spinnerComponent = this.container.createComponent(this.componentFactory);
      this.spinnerComponent.instance.message = this.message;
    } else {
      this.container.createEmbeddedView(this.template);
    }
  }

  @Input('appSpinnerMessage')
  set spinnerMessage(message: string) {
    this.message = message;
  }

  componentFactory: ComponentFactory<SpinnerComponent>;
  spinnerComponent: ComponentRef<SpinnerComponent>;

  constructor(private container: ViewContainerRef,
              private template: TemplateRef<any>,
              private componentFactoryResolver: ComponentFactoryResolver) {
    this.componentFactory = this.componentFactoryResolver.resolveComponentFactory(SpinnerComponent);
  }

  ngOnInit(): void {

  }

  @HostListener('click', ['$event'])
  public onClick(event: any): void {
    event.stopPropagation();
  }

  @HostListener('mousedown', ['$event'])
  public onMouseDown(event: any): void {
    event.stopPropagation();
  }

  @HostListener('mouseup', ['$event'])
  public onMouseUp(event: any): void {
    event.stopPropagation();
  }
}

Few things to note here, we are passing a boolean value to our directive via the input @Input('appSpinner') and a string value for the loader message via the input @Input('appSpinnerMessage'). The boolean decides whether or not to dynamically create the SpinnerComponent and inject it into the view container.

onMouseUp, onMouseDown and onClick are decorated with @HostListener which tells Angular that when an event happens on the host i.e. the div on which the spinner shows up, call the decorated method. In these methods, we are simply blocking all the mouse click events. We don’t want the user to interact with the underlying contents while the spinner is showing.

We have everything that we need. All that’s left to be done is adding SpinnerComponent and AppSpinnerDirective to our module. Also, since we’re injecting SpinnerComponent as a dynamic component, do not forget to add it to the entryComponents.

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';

import {AppComponent} from './app.component';
import {SpinnerComponent} from './components/spinner/spinner.component';
import {AppSpinnerDirective} from './directives/app-spinner.directive';

@NgModule({
  declarations: [
    AppComponent,
    SpinnerComponent,
    AppSpinnerDirective
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  entryComponents: [
    SpinnerComponent
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

That’s it. Now how do we use it? That is a breeze.

All you need to do is use *appSpinner directive in your html element.

<div *appSpinner="isLoading; message: 'Division is Loading'"> // isLoading is a condition check for showing/hiding spinner
  Some Content
</div>

Demo

Try it yourself here.

Source Code

Get the entire source code for this project with implementation and example from this Git Repository.

Leave a Reply

%d bloggers like this: