DEV Community

Cover image for How to add loading spinner to Angular material button
Dzmitry Hutaryan
Dzmitry Hutaryan

Posted on

How to add loading spinner to Angular material button

Angular material is a good UI library. For sure, we have a button component there. It works good except one thing - loading state.

Probably you (and me as well) used something like that (pseudocode)

<button> @if (loading) { <spinner /> } Text <button/> 
Enter fullscreen mode Exit fullscreen mode

It might be okay but let's do out live a bit easier :) We will create a directive that will add spinner to our buttons based on input value.

Init the directive

First, we init our directive and call it ButtonLoading (you can call whatever you want). As selector we will use button[matButton][loading]. So, our directive will be applied for material buttons with loading input (works for Angular 20+, you might change it for your Angular version).

We need two method for create spinner and destroy it. Both methods will be triggered when loading input changes.

import { Directive, effect, input } from '@angular/core'; @Directive({ selector: 'button[matButton][loading]', }) export class ButtonLoading { loading = input(false); constructor() { effect(() => { if (this.loading()) { return this._createSpinner(); } return this._destroySpinner(); }); } private _createSpinner() {} private _destroySpinner() {} } 
Enter fullscreen mode Exit fullscreen mode

Spinner

For spinner we will use already existed component MatProgressSpinner. We also need ViewContainerRef to create component, Renderer2 to create DOM-node inside MatButton and inject MatButton to get nativeElement.

Now our createSpinner method looks in this way. I've left some comments for you.

 get nativeElement(): HTMLElement { return this._matButton._elementRef.nativeElement; } private _createSpinner() { // do nothing if spinned exists if (this._spinner) return; // create spinner this._spinner = this._viewContainerRef.createComponent(MatProgressSpinner); // set diameter this._spinner.instance.diameter = 16; // set mode as infinity this._spinner.instance.mode = 'indeterminate'; // create DOM-node this._renderer.appendChild( this.nativeElement, this._spinner.instance._elementRef.nativeElement ); } 
Enter fullscreen mode Exit fullscreen mode

Other method destroySpinner will be much easier:

 private _destroySpinner() { if (!this._spinner) return; this._spinner.destroy(); this._spinner = null; } 
Enter fullscreen mode Exit fullscreen mode

Now we can see our spinner. But we see the button text and spinner at the same time. Let's hide text and show spinner by center. We need to add a css class and some styles.

First, we'll add a class to know when we have spinner which was added by our directive.

effect(() => { if (this.loading()) { this._renderer.addClass(this.nativeElement, 'mdc-button-loading'); return this._createSpinner(); } this._renderer.removeClass(this.nativeElement, 'mdc-button-loading'); return this._destroySpinner(); }); 
Enter fullscreen mode Exit fullscreen mode

Then we'll just hide label and make spinner an absolutely positioned.

.mdc-button-loading { .mdc-button__label { visibility: hidden; } .mat-mdc-progress-spinner { position: absolute; } } 
Enter fullscreen mode Exit fullscreen mode

Improvements

It's a good option to make button disabled when loading is active. Let's do it!

Add an input disabled (the same as material button has). Important to use transform option.

disabled = input(false, { transform: booleanAttribute }); 
Enter fullscreen mode Exit fullscreen mode

Then add this state to effect. When we active loading we always disable button but when we deactivate it we use disabled value from the outside.

effect(() => { if (this.loading()) { this._renderer.addClass(this.nativeElement, 'mdc-button-loading'); this._matButton.disabled = true; return this._createSpinner(); } this._renderer.removeClass(this.nativeElement, 'mdc-button-loading'); this._matButton.disabled = this.disabled(); return this._destroySpinner(); }); 
Enter fullscreen mode Exit fullscreen mode

That's it. You can modify it as you wish for your needs. You will find the complete result below.

Top comments (0)