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/>
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() {} }
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 ); }
Other method destroySpinner
will be much easier:
private _destroySpinner() { if (!this._spinner) return; this._spinner.destroy(); this._spinner = null; }
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(); });
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; } }
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 });
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(); });
That's it. You can modify it as you wish for your needs. You will find the complete result below.
Top comments (0)