DEV Community

Cover image for Reusable Loader directive - Angular
devlazar
devlazar

Posted on

Reusable Loader directive - Angular

Table Of Contents
* πŸš€INTRO
* πŸ§ͺ EXAMPLE USE CASE
* βš™ IMPLEMENTATION
* πŸ™THANK YOU

πŸš€ INTRO

Hi all! Hope you are all having a great weekend. I am currently working on multiple platforms using ReactJS, Angular 12 and Node.js.


I noticed that Angular is kinda hard to work with in terms of having reusable and easy to use components for loading, empty, error state and similar. So, I wanted to figure out the way of how to create a simple relative ("relative" in terms of where it is placed, which means it is placed in the relative parent element within the HTML)loader component that could just be plugged in into the component and control the loading of the specific component.

πŸ§ͺ EXAMPLE USE CASE

Let's consider this:

You are employee of the Stark industries. Tony rings you up:

Hey, man. Can you make a simple UI form that will allow me to select the suit type, color and reactor type. The form should be in the dialog?

Of course you would say yes, It is a freakin' Tony Stark xD

You start listing the requirements:

  • Dialog component

  • Should have 3 inputs, suit type, color and reactor type

  • We need to fetch available suits, colors and reactor (let's assume we can fetch it all from one endpoint)

  • The user should not be able to interact with the component unless everything is loaded

Let's see how to implement this!

journey

βš™ [IMPLEMENTATION]

Our main dialog component (ts file) will have **status* class member which will tell us if the API call is triggered. It will also have a form group and of course the data that will provide us with the type of suits, colors and reactor types. Of course, we have to create our form and function that will call our API service. It will look something like this.

interface IDataResponse {
  suits: Array<any>;
  colors: Array<string>;
  reactors:Array<any>
}

@Component({
  selector: "stark-dialog",
  templateUrl: "./stark-dialogcomponent.html",
  styleUrls: ["./stark-dialog.component.scss"],
})
export class StarkDialogComponent implements OnInit {
  status: 'loading' | 'not-loading' = 'not-loading';
  starkForm!: FormGroup;
  data: Array<IDataResponse> = [];

  constructor(
    @Inject(MAT_DIALOG_DATA) public data: IDialogData,
    private _apiService: ApiService,
    private _formBuilder: FormBuilder,
  ) {
    this.createStarkForm();
  }

  ngOnInit(): void {
    this.status = 'loading';
    this.fetchData();
  }

  fetchData(): void {
    ...data fetching logic
    ...
    this.status = 'not-loading';
  }

  createStarkForm() {
    this.starkForm = this._formBuilder.group({
      selectSuitType: ["", Validators.required],
      selectColor: ["", Validators.required],
      selectReactorType: ["", Validators.required],
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Our HTML could look something like this

<h2 mat-dialog-title>
  Choose suit setup
</h2>
<mat-dialog-content>
      <form [formGroup]="starkForm" (submit)="onSubmit()" fxLayout="column">
        <mat-form-field appearance="fill">
          <mat-label>Select suit type</mat-label>
          <mat-select name="selectSuitType">
            <mat-option *ngFor="let suit of data.suits" [value]="suit">
              {{ suit.type }}
            </mat-option>
          </mat-select>
        </mat-form-field>
        <mat-form-field appearance="fill">
          <mat-label>Select color</mat-label>
          <mat-select name="selectColor">
            <mat-option *ngFor="let color of data.colors" [value]="color">
              {{ color.key }}
            </mat-option>
          </mat-select>
        </mat-form-field>
        <mat-form-field appearance="fill">
          <mat-label>Select reactor type</mat-label>
          <mat-select name="selectReactorType">
            <mat-option *ngFor="let reactor of data.reactors" [value]="reactor">
              {{ reactor.type }}
            </mat-option>
          </mat-select>
        </mat-form-field>
      </form>
</mat-dialog-content>
<mat-dialog-actions align="end">
  <button mat-stroked-button color="primary" type="button" mat-dialog-close>
    Cancel
  </button>
  &nbsp;
  <button mat-raised-button color="primary">Submit</button>
</mat-dialog-actions>
Enter fullscreen mode Exit fullscreen mode

Our form would then look something like this:

Form image

The idea is to prevent user seeing this form before all the data is loaded. We could find solution for that using the Angular Directive.

@Directive({
  selector: "[relativeLoader]",
})
export class RelativeLoaderDirective implements OnInit, OnChanges {
  private loader: HTMLElement;
  @Input() loading: boolean = false;
  constructor(private renderer: Renderer2, private el: ElementRef) {
    this.loader = this.renderer.createElement("div"); // create loader
  }
  ngOnInit(): void {}

  ngOnChanges(): void {
    this.createSimpleLoader(); // execute create loader
    if (this.loading && this.el) {
      // hide the first element in the parent div containing directive
      // this should always be a component you want to replace with
      // the loader we are making
      this.renderer.setStyle(
        this.el.nativeElement.firstChild,
        "display",
        "none"
      );
      this.renderer.appendChild(this.el.nativeElement, this.loader);
    } else {
      this.renderer.removeChild(this.el.nativeElement, this.loader);
      this.renderer.setStyle(
        this.el?.nativeElement.firstChild,
        "display",
        "block"
      );
    }
  }

  createSimpleLoader() {
    /** add some style to the loader wrapper */
    this.renderer.setStyle(this.loader, "display", "flex");
    this.renderer.setStyle(this.loader, "flex-direction", "column");
    this.renderer.setStyle(this.loader, "justify-content", "center");
    this.renderer.setStyle(this.loader, "align-items", "center");
    // create loader spinner with custom scss
    /** Format of this loader is:
        <div class="lds-roller">
            <div></div>
            <div></div>
            <div></div>
            <div></div>
                <div></div>
                <div></div>
                <div></div>
               <div></div>
        </div>
    */
    const ldsRoller = this.renderer.createElement("div");
    this.renderer.addClass(ldsRoller, "lds-roller");
    [0, 1, 2, 3, 4, 5, 6, 7].forEach((value) => {
      const div = this.renderer.createElement("div");
      this.renderer.appendChild(ldsRoller, div);
    });
    this.renderer.appendChild(this.loader, ldsRoller);
  }
}
Enter fullscreen mode Exit fullscreen mode

SCSS for the loader (taken from https://loading.io/css/)

.lds-roller {
  display: inline-block;
  position: relative;
  width: 80px;
  height: 80px;
}
.lds-roller div {
  animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
  transform-origin: 40px 40px;
}
.lds-roller div:after {
  content: " ";
  display: block;
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fff;
  margin: -4px 0 0 -4px;
}
.lds-roller div:nth-child(1) {
  animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
  top: 63px;
  left: 63px;
}
.lds-roller div:nth-child(2) {
  animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
  top: 68px;
  left: 56px;
}
.lds-roller div:nth-child(3) {
  animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
  top: 71px;
  left: 48px;
}
.lds-roller div:nth-child(4) {
  animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
  top: 72px;
  left: 40px;
}
.lds-roller div:nth-child(5) {
  animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
  top: 71px;
  left: 32px;
}
.lds-roller div:nth-child(6) {
  animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
  top: 68px;
  left: 24px;
}
.lds-roller div:nth-child(7) {
  animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
  top: 63px;
  left: 17px;
}
.lds-roller div:nth-child(8) {
  animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
  top: 56px;
  left: 12px;
}
@keyframes lds-roller {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
Enter fullscreen mode Exit fullscreen mode

IMPORTANT STUFF!

In order for this to work, you should provide this format of the HTML:

<div relativeLoader [loading]="your_loading_indicator">
    <div>
      <content-you-want-to-replace-with-loader />
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The way it works:

Directive will find reference the parent it refers to. It will find the first child which is the first div, It will hide it and add the loader at the end of the parent element.

Let's update our HTML

<h2 mat-dialog-title>
  Choose suit setup
</h2>
<mat-dialog-content>
  <div relativeLoader [loading]="isLoading === 'loading'">
     <div>
      <form [formGroup]="starkForm" (submit)="onSubmit()" fxLayout="column">
        <mat-form-field appearance="fill">
          <mat-label>Select suit type</mat-label>
          <mat-select name="selectSuitType">
            <mat-option *ngFor="let suit of data.suits" [value]="suit">
              {{ suit.type }}
            </mat-option>
          </mat-select>
        </mat-form-field>
        <mat-form-field appearance="fill">
          <mat-label>Select color</mat-label>
          <mat-select name="selectColor">
            <mat-option *ngFor="let color of data.colors" [value]="color">
              {{ color.key }}
            </mat-option>
          </mat-select>
        </mat-form-field>
        <mat-form-field appearance="fill">
          <mat-label>Select reactor type</mat-label>
          <mat-select name="selectReactorType">
            <mat-option *ngFor="let reactor of data.reactors" [value]="reactor">
              {{ reactor.type }}
            </mat-option>
          </mat-select>
        </mat-form-field>
      </form>
  </div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
  <button mat-stroked-button color="primary" type="button" mat-dialog-close>
    Cancel
  </button>
  &nbsp;
  <button mat-raised-button color="primary">Create</button>
</mat-dialog-actions>
Enter fullscreen mode Exit fullscreen mode

Add the end we should get something like this

Loading gif

So, the only thing to worry about is that you have a class member that will control the loading state. But, If you use the provided template you should be able to reuse this loader directive across entire application.

πŸ™ THANK YOU FOR READING!

Please leave a comment, tell me about you, about your work, comment your thoughts, connect with me!

β˜• SUPPORT ME AND KEEP ME FOCUSED!

Buy Me a Coffee at ko-fi.com

Have a nice time hacking! 😊

Latest comments (0)