Writing a simple “busy” spinner Directive in Angular-Material

Aaron Moore
CodeX
Published in
4 min readAug 16, 2021

--

Angular Logo

As a front-end developer in the world of async service calls it’s important to give feedback to your user. However, it’s a pain to do this on a case-by-case basis through a whole lot of *ngIf and switching based upon some sort of boolean flag. Let’s write a structural directive to handle this for us with as little work as possible.

The structural directive we’re about to write is going to take a boolean expression, and swap the contents of the element it’s attached to. When the expression is true it will swap in a MatProgressSpinner component and when it’s false we’ll display what the user has coded into their component.

Here’s a demo:

Keep it DRY

The importance of this isn’t simply to find a way to provide the user with visible feedback, but also to do it in a consistent manner that conforms to the DRY principle where we don’t repeat code by using copy/paste all over the place.

DRY: Don’t repeat yourself

One way to implement the equivalent of this without conforming to DRY is simply:

<div *ngIf="isLoading"><mat-spinner [diameter]="18"></mat-spinner></div>
<div *ngIf="!isLoading">
<entity-details></entity-details>
</div>

However, in order to reuse this, you’d have to copy/paste that spinner code. Even if simplified that into it’s own component we’d end up with only slightly less copy/paste with the 2 opposing *ngIf statements. However, if we do it in a structural directive we can use the equivalent of writing a single *ngIf statement to accomplish the same goal.

So, how’s it work?

You can get the full source from the StackBlitz above within the spin-on-directive.ts with a small styling tidbit in styles.scss but the important bit of the directive code looks like this:

@Input() set appSpinOn(condition: boolean) {
if (!!condition !== this.#isSpinning) {
this.#spinner = null;
this.viewContainer.clear();
this.#isSpinning = condition;
if (!condition) {
// Render the template
this.viewContainer.createEmbeddedView(this.templateRef);
} else if (condition) {
this.addSpinner();
}
}
}
private addSpinner() {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(MatProgressSpinner);
const { instance } = this.viewContainer.createComponent<MatProgressSpinner>(componentFactory);
instance.diameter = this.#diameter;
instance.color = this.#color;
instance.mode = 'indeterminate';
instance._elementRef.nativeElement.classList.add('spin-on-instance');
this.#spinner = instance;
}
}

The setter for the appSpinOn input value takes the expression evaluating to a boolean. If the expression has evaluated before to the same result, it won’t do anything so it just leaves what’s in the dom. If it hasn’t evaluated, or the value is different, it will determine whether the condition is true or false. Here we use this.viewContainer.createEmbeddedView(this.templateRef); to write the templateRef that the directive is attached to if the condition is false.

However, if the condition is true things get a little more complicated. We use dynamic component creation to create the MatProgressSpinner within the ViewContainerRef that Angular injects to the directive, and then configure it for styling, and then

We’ll also need to add some styling, otherwise we won’t be able to use it in place for subbing out things like icons. I put the following in the root styles.scss file which allows for appropriate alignment and margins to work in place of an icon:

.mat-progress-spinner.spin-on-instance {
vertical-align: middle;
display: inline-block;
margin: 3px;
}

From there, using it is as simple as adding SpinOnModule the imports of any module using it and then using it in code.

One way of using it is to sub out content. For example:

<div>
<entity-details *appSpinOn="isLoading"></entity-details>
</div>

The defaults are useful for subbing out icons. For example, the demo uses the following on a “Save” button:

<button mat-raised-button [disabled]="isSaving || isLoading" (click)="save()">
<mat-icon *appSpinOn="isSaving">save</mat-icon> Save
</button>
A save button with an icon
A save button where the icon is changes to a progress spinner

Before saving, the image has a “save” icon. The directive substitutes just the icon with the spinner.

We can also use the variables color which takes an angular-material ThemePalette string and a numeric diameter to change the size the directive uses: *appSpinOn="isLoading; color: 'primary': diameter: 40".

What we’ve made here is a useful directive that gives the user feedback when something is going on behind-the-scenes. It reduces the amount of code we have to write, it’s flexible, and configurable.

--

--