Reusable Loader directive - Angular
devlazar
Posted on July 9, 2022
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!
β [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],
});
}
}
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>
<button mat-raised-button color="primary">Submit</button>
</mat-dialog-actions>
Our form would then look something like this:
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);
}
}
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);
}
}
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>
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>
<button mat-raised-button color="primary">Create</button>
</mat-dialog-actions>
Add the end we should get something like this
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!
Have a nice time hacking! π
Posted on July 9, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.