Reusable Loader directive - Angular
Tags: angular, typescript, programming, webdev
Published: 2022-07-09 12:39:20

Table Of Contents
        * 🚀[INTRO](#intro)
        * 🧪 [EXAMPLE USE CASE](#use-case)
        * ⚙ [IMPLEMENTATION](#implementation)
        * 🙏[THANK YOU](#thank-you)

## 🚀 INTRO <a name="intro"></a>

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 <a name="use-case"></a>

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](https://media.giphy.com/media/3lvqNXheb679S/giphy.gif)

## ⚙ [IMPLEMENTATION] <a name="implementation"></a>

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.

```typescript
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],
    });
  }
}
```

Our HTML could look something like this

```html
<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>
```

Our form would then look something like this:


![Form image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1g5defkrlpkqkzao5z58.png)

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**.

```typescript
@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);
  }
}
```

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

```scss
.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);
  }
}
```

---
**IMPORTANT STUFF!**

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

```html
<div relativeLoader [loading]="your_loading_indicator">
    <div>
      <content-you-want-to-replace-with-loader />
    </div>
</div>
```
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**


 ```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>
```

Add the end we should get something like this


![Loading gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sovvmfhrz22lmh0vb2ks.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!<a name="thank-you"></a>

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

☕ SUPPORT ME AND KEEP ME FOCUSED!

<a href='https://ko-fi.com/Z8Z33D1HJ' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://cdn.ko-fi.com/cdn/kofi4.png?v=2' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>

Have a nice time hacking! 😊