Reputation: 515
I'm trying to write a directive to modify a button to show a spinner button while a server operation is in progress.
<button type="submit" waitingButton (click)="submit($event)">
{{submitBtnText}}
</button>
So far, I've been able to show the spinner, but I don't know how to capture the result from the click event to switch it back to normal.
This is the directive:
import {
Directive,
ElementRef,
HostListener,
OnInit,
Renderer2,
ComponentFactoryResolver,
ViewContainerRef
} from '@angular/core';
import { MatSpinner } from '@angular/material';
@Directive({
selector: '[bcnWaitingButton]'
})
export class BCNWaitingButtonDirective implements OnInit {
constructor(
private el: ElementRef,
private renderer: Renderer2,
private componentFactoryResolver: ComponentFactoryResolver,
public vcRef: ViewContainerRef) {
}
ngOnInit(): void {
}
@HostListener('click', ['$event']) onClick(e): void {
// Create the spinner
const factory = this.componentFactoryResolver.resolveComponentFactory(MatSpinner);
const componentRef = this.vcRef.createComponent(factory);
const spinner: MatSpinner = componentRef.instance;
// Configure the spinner
spinner.strokeWidth = 3;
spinner.diameter = 24;
// Set the button to disabled
this.renderer.setAttribute(this.el.nativeElement, 'disabled', 'true');
// Apply new styles
const span: ElementRef = this.el.nativeElement.querySelector('.mat-button-wrapper');
this.renderer.setStyle(span, 'display', 'flex');
this.renderer.setStyle(span, 'align-items', 'center');
this.renderer.setStyle(span, 'justify-content', 'center');
this.renderer.setStyle(spinner._elementRef.nativeElement, 'margin-top', '0px');
this.renderer.setStyle(spinner._elementRef.nativeElement, 'margin-left', '5px');
// Append the spinner
this.renderer.appendChild(this.el.nativeElement.firstChild, spinner._elementRef.nativeElement);
}
}
Any ideas on how to do that? Should I have an @Input with the submit callback so I can call it from the onClick function of the directive?
Upvotes: 6
Views: 7079
Reputation: 650
Here is a full working directive for replacing a Material button's text with a MatSpinner and then back to the text when the operation is complete. It utilizes parts of the first two answers:
import {
ComponentFactoryResolver, Directive, Input, OnChanges, OnInit, Renderer2, SimpleChanges, ViewContainerRef
} from '@angular/core';
import { ElementRef } from '@angular/core';
import { MatSpinner } from '@angular/material';
@Directive({
selector: 'button[appShowSpinner]'
})
export class SpinnerButtonDirective implements OnInit, OnChanges {
// tslint:disable-next-line:no-input-rename
@Input('appShowSpinner') showSpinner: boolean;
originalInnerText: string;
spinner: MatSpinner;
constructor(
private el: ElementRef,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver
) { }
ngOnInit() {
// Record the button's original text
this.originalInnerText = this.el.nativeElement.innerText;
// Set the button to maintain the same dimensions, even once we put the spinner inside.
this.el.nativeElement.style.width = `${(this.el.nativeElement as HTMLElement).offsetWidth}px`;
this.el.nativeElement.style.height = `${(this.el.nativeElement as HTMLElement).offsetHeight}px`;
// Create the spinner
const factory = this.componentFactoryResolver.resolveComponentFactory(MatSpinner);
const componentRef = this.viewContainerRef.createComponent(factory);
this.spinner = componentRef.instance;
// Configure the spinner
this.spinner.strokeWidth = 3;
this.spinner.diameter = 24;
// Hide the spinner
this.renderer.setStyle(this.spinner._elementRef.nativeElement, 'display', 'none');
// Apply new styles to the button content's container
const span = this.el.nativeElement.querySelector('.mat-button-wrapper') as HTMLSpanElement;
this.renderer.setStyle(span, 'display', 'flex');
this.renderer.setStyle(span, 'align-items', 'center');
this.renderer.setStyle(span, 'justify-content', 'center');
}
ngOnChanges(changes: SimpleChanges) {
if (typeof(changes.showSpinner) === 'object' && !changes.showSpinner.isFirstChange()) {
if (changes.showSpinner.currentValue === true) {
// Clear the button's text
const span = this.el.nativeElement.querySelector('.mat-button-wrapper') as HTMLSpanElement;
span.innerText = '';
// Append the spinner
this.renderer.appendChild(this.el.nativeElement.firstChild, this.spinner._elementRef.nativeElement);
// Show the spinner
this.renderer.setStyle(this.spinner._elementRef.nativeElement, 'display', 'inherit');
}
if (changes.showSpinner.currentValue === false) {
// Hide the spinner
this.renderer.setStyle(this.spinner._elementRef.nativeElement, 'display', 'none');
// Remove the spinner
this.renderer.removeChild(this.el.nativeElement.firstChild, this.spinner._elementRef.nativeElement);
const span = this.el.nativeElement.querySelector('.mat-button-wrapper') as HTMLSpanElement;
span.innerText = this.originalInnerText;
}
this.el.nativeElement.disabled = changes.showSpinner.currentValue;
}
}
}
Using the directive:
<button [appShowSpinner]="isSaving" mat-raised-button>
Submit
</button>
Upvotes: 10
Reputation: 1286
I just made an example that works ( although I cannot use material spinner in stackblitz because of a dependency error ) but this should guide you :
import { Directive, Input, OnChanges } from '@angular/core';
import { ElementRef } from '@angular/core';
@Directive({
selector: '[spinnerButton]'
})
export class SpinnerButtonDirective {
@Input('spinnerButton') isWaiting: boolean;
originalInnerText: string;
constructor( private el: ElementRef ) { }
ngOnInit(){
// Save the original button text so I can restore it when waiting ends
this.originalInnerText = this.el.nativeElement.innerText;
}
ngOnChanges() {
if (this.isWaiting) {
this.el.nativeElement.innerText = 'waiting...';
} else {
if (this.el.nativeElement.innerText == 'waiting...') {
this.el.nativeElement.innerText = this.originalInnerText;
}
}
this.el.nativeElement.disabled = this.isWaiting;
}
}
In your html,
<button (click)="getData()" [spinnerButton]="isWaiting">Get Data</button>
The only thing you have to do then, is set isWaiting to true or false in your code ( normally in getData() function )
Demo : https://stackblitz.com/edit/angular-rb5vmu
Upvotes: 0