Curtis
Curtis

Reputation: 3449

Angular how to get button element on form submit

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

Answers (5)

user6576367
user6576367

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

Kamil Kiełczewski
Kamil Kiełczewski

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

AVJT82
AVJT82

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

adam0101
adam0101

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

Saksham
Saksham

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

Related Questions