Reputation: 3449
I have a page with multiple forms on it. Each form has a number of buttons on it. I would like to implement a loading spinner on buttons after they are pressed. When I use a normal click event I can pass in the button:
HTML
<button #cancelButton class="button-with-icon" type="button" *ngIf="hasCancel" mat-raised-button color="warn" [disabled]="isLoading" (click)="clickEvent(cancelButton)">
<mat-spinner *ngIf="isLoading" style="display: inline-block;" diameter="24"></mat-spinner>
Cancel
</button>
TS
clickEvent(button: MatButton) {
console.log(button);
}
In this case the button element gets passed through and you can access it in order to add a loading class to the button.
However, if you try the same thing using a submit button it comes through as undefined
:
HTML
<form (ngSubmit)="save(saveButton)">
<button #saveButton class="button-with-icon" type="submit" *ngIf="hasSave" mat-raised-button color="primary" [disabled]="isLoading">
<mat-spinner *ngIf="isLoading" style="display: inline-block;" diameter="24"></mat-spinner>
Save
</button>
</form>
TS
save(button: MatButton) {
console.log(button);
}
In this case button is undefined
because the *ngIf
on the button blocks it from the scope of the form. I could remove the *ngIf
and just hide the button, but this leaves the button on the DOM and I do not want to do that.
Here is a stack blitz: https://stackblitz.com/edit/angular-zh7jcw-mrqaok?file=app%2Fform-field-overview-example.html
I have tried adding an additional click event to the save button to set the button to loading but the click event fires first and set the button to disabled before blocking the submit event from being called.
Ive looked through the submit event but cant see anything that links back to the button that was clicked.
Any suggestions?
Upvotes: 5
Views: 14317
Reputation:
tl;dr Here is my solution in action: https://stackblitz.com/edit/angular-zh7jcw-ythpdb
Edit: As I just noticed, @AJT_82 explains OP's original problem with the reference element very well. So I now removed my earlier answer.
However I would suggest the following solution that would fit much better in terms of separation of concerns and doing things in angular way, that is by introducing two new members cancelActive
and submitActive
, you will be able to handle the different states of the form more flexibly.
Here you can see it in action: https://stackblitz.com/edit/angular-zh7jcw-ythpdb
export class FormFieldOverviewExample {
cancelActive = false;
submitActive = false;
// this is for submit event
saveEvent(event: Event) {
// additional logic can be added
// if(!this.cancelActive) return;
this.submitActive = true;
this.cancelActive = false;
}
// this is for cancel event
cancelEvent(event: Event) {
// additional logic can be added
// if(!this.submitActive) return;
this.submitActive = true;
this.cancelActive = false;
}
}
<form (ngSubmit)="saveEvent($event)">
<button class="button-with-icon" type="button" *ngIf="hasCancel" mat-raised-button color="warn" [disabled]="cancelActive" (click)="cancelEvent($event)">
<mat-spinner *ngIf="cancelActive" class="spinner" diameter="15"></mat-spinner>
Cancel
</button>
<br><br>
<button class="button-with-icon" type="submit" *ngIf="hasSave" mat-raised-button color="primary" [disabled]="submitActive">
<mat-spinner *ngIf="submitActive" class="spinner" diameter="15"></mat-spinner>
Save
</button>
</form>
This way you would also have more playground, such as:
<button type="submit" [disabled]="submitActive || cancelActive">...</button>
You can simply disable the submit
button without showing the loading animation for it, if you want to do it for some reason...
Further, suppose you have an <input type="text" />
element in your form. User switches to that element (via click or tab press) and after he is done with editing he presses the Enter key, in this case, the form is also submitted automatically by the browser; so this would make things even more complicated in your existing code structure.
Upvotes: 2
Reputation: 92377
Instead of using the ngSubmit
event handler on the form use a click
event handler on the save button. Leave the type as submit
so that it still works when the user uses the Enter
key on an input control to submit the form.
<form #formElement="ngForm">
...
<button #saveButton (click)="save(saveButton, formElement)" type="submit" ...>
<mat-spinner ...></mat-spinner>
Save
</button>
</form>
You can also avoid needing to add the loading
class to the DOM by using an object to keep track of which button is in the loading state. Working example here. This also allows you to have many buttons without having to create a fixed variable for each button.
class FormFieldOverviewExample {
hasCancel = true;
hasSave = true;
isLoading = {};
saveEvent(event, buttonName, form) {
console.log(buttonName);
if(form.valid) {
this.isLoading[buttonName]=true;
// ...
} else {
this.isLoading[buttonName]=false;
console.warn("Form not submitted because it contains errors");
}
}
clickEvent(event, buttonName) {
console.log(buttonName);
this.isLoading[buttonName]=true;
// ...
}
}
form { display: none; }
<form #formElement="ngForm">
...
<button (click)="clickEvent($event, 'cancelButton')" [disabled]="isLoading['cancelButton']" *ngIf="hasCancel" type="button" ...>
<mat-spinner *ngIf="isLoading['cancelButton']" ...></mat-spinner>
Cancel
</button>
<br><br>
<button (click)="saveEvent($event, 'saveButton', formElement)" [disabled]="isLoading['saveButton']" *ngIf="hasSave" type="submit" ...>
<mat-spinner *ngIf="isLoading['saveButton']" ...></mat-spinner>
Save
</button>
</form>
Upvotes: 3
Reputation: 73357
This actually has nothing to do with the submit function, but all to do with angular and structural directives. When you apply a structural directive (ngFor
, ngIf
...) what angular does under the hood is to create a ng-template
node. What this means, that template reference variables that are defined in the scope of the structural directive is only available in that node.
So what you have here:
<button #saveButton *ngIf="hasSave">
Save
</button>
means that saveButton
template reference is only available inside that ng-template
which is created for the button, and therefore not available in the submit function.
What we have here:
<button #cancelButton *ngIf="hasCancel" (click)="clickEvent(cancelButton)">
Cancel
</button>
is that the click event is on the button, so cancelButton
template reference variable is available for that event.
Using hidden
instead of ngIf
like suggested in other answer, solves this issue, since we just.. well, hide the element.
Upvotes: 2
Reputation: 30995
Update I understand your particular issue now. The problem is the *ngIf
on the button. It's breaking the reference. Change it to [hidden]
and it should work. Here's an updated StackBlitz: https://stackblitz.com/edit/angular-zh7jcw-pcuuyv
As for why it happen, I think this describes the problem pretty well: https://stackoverflow.com/a/36651625/64279
Original answer:
I wouldn't even pass it in. Just add this in your class:
@ViewChild('saveButton') button;
Then you can reference it in your save method:
save() {
console.log(this.saveButton);
}
But I will also add that calling function from interpolation is not a good idea as it can cause performance issues. Rather, you should subscribe to an event which sets a property, then reference that property in your view. That way, the function is only called when the event fires instead of every rendering of the page.
Upvotes: 3
Reputation: 9380
FOr your case, you should not pass the element to the function and then manipulate the class on it but rather simply maintain a variable for loading state. In your component, initialize it as
loading: boolean = false;
And in your template, bind this class as
[class.loading]="loading"
In your method, set this property initially to true and then to false once the async call is complete. Similary use the same variable to set disabled attribute as
[disabled]="loading"
Upvotes: 1