23

If I click fast on my submit-button the form is submitted two or more times. My thought was to prevent this with the disabled attribute, but I need variable disableButon in every form like this:

@Component({
    selector: 'example',
    template: `
      <form (submit)="submit()" >
        <--! Some Inputs -->
        <button [disabled]="disableButton" type="submit">Submit<button>
      </form>
       `
  })
  export class ExampleComponent {
    private disableButton: boolean = false;
    .......
    submit(){
      this.disableButton = true;
      /*
      * API call
      */
      this.disableButton = false;
    }
  }

Am I doing this right or is there a more efficent/elegant way to do it?

Daskus
  • 929
  • 1
  • 10
  • 25
  • this is the thread handling the problem properly via an Angular directive: https://stackoverflow.com/questions/51390476/how-to-prevent-double-click-in-angular – Sebastian Viereck Jun 21 '23 at 08:06

6 Answers6

13

Dealing with double-submission is easy to do wrong. On a form with <form #theForm="ngForm" (ngSubmit)="submit()">:

<button type="submit" [disabled]="theForm.submitted" /> would only work if no validation whatsoever was present. Angular's ngForm.submitted is set to true when the button is pressed, not after the form passes validation. (NgForm's "submitted" property actually means "tried to submit".)

<button type="submit" [disabled]="theForm.submitted && theForm.valid" /> isn't much better: after getting validation errors on submission, the moment the user fixes the validation errors, the submit button disables itself right as they're reaching for it to re-submit.

Resetting ngForm.submitted either directly or via ngForm.resetForm() within your component's submit() is a poor option, since submitted is your primary variable controlling whether and where the validation error messages are displayed.

The real problem: Angular has no way to know when or whether your API calls in submit() failed or succeeded. Even if Angular provided a property that meant "just clicked Submit button and it also passed all validation" on which you can hang [disabled]="thatProperty", Angular wouldn't know when to set the property back, such as when your API call errors out and you'd like to let the user press submit again to re-try the server.

Perhaps Angular might proscribe all submit functions to be of the form () => Observable<boolean> and it could subscribe to your submit's success or failure, but it seems overkill just to reset a boolean in the framework.

So you must take action after all your API calls are finished and inform Angular somehow that the submit button is ready for reuse. That action is either going to be setting the explicit boolean you are already doing, or imperatively disabling.

Here's how to do it imperatively, without the boolean.

Add a template reference variable like #submitBtn to the submit button:

<button type="submit" #submitBtn class="green">Go!</button>

Pass it to your component's submit():

<form (ngSubmit)="submit(submitBtn)" ...>

Accept and use it component-side:

submit(submitBtn: HTMLButtonElement): void {
    submitBtn.disabled = true;
    /// API calls
    submitBtn.disabled = false;
}

And if your API calls have multiple pathways that share a common error-handler, you'd need to pass the HTMLButtonElement on through to them as well, since they can no longer pluck it out of the component with this.disableButton.

(Alternately, instead of declaring and passing #submitBtn, you already have #theForm declared, so pass that instead as :NgForm, and component code can drill-down to the button... or to an overlay over the whole form, or whatever.)

Whether this solution is more or less elegant than declaring another boolean that works slightly differently than ngForm.submitted is opinion, but it is fact that Angular can't know when the component's submit() and all its async processes are finished without a subscription.

Ron Newcomb
  • 2,886
  • 21
  • 24
  • you wrote "[...] pass that instead as :NgForm, and component code can drill-down to the button [...]" - can you provide an example how to does this? can't figure out where I can find the button within the NgForm object. Thank you! – Dirk Jul 19 '18 at 11:24
  • 1
    +1 Best method I've seen. On submit methods that have a single API call I find it elegant to reenable the submit button in the finalize operator on the observable: `.pipe(finalize() => { submitButton.disabled = false; })).subscribe(...` – pumpkinthehead Oct 29 '19 at 22:10
  • Missing bracket: `.pipe(finalize(() => { submitButton.disabled = false; })).subscribe(` – mkczyk Jun 01 '20 at 09:35
5

This should work as well:

<button #button (ngSubmit)="button.disabled = true" type="submit">Submit<button>

or just (click) instead of (ngSubmit)

update (see comments)

<button #button [disabled]="!form.valid || button.hasAttribute('is-disabled')"
     (ngSubmit)="button.setAttribute('is-disabled', 'true')"
     type="submit">Submit<button>

update (use a directive)

@Directive({
  selector: 'button[type=submit]'
})
class PreventDoubleSubmit {

  @HostBinding() disabled:boolean = false;

  @Input() valid:boolean = true;      

  @HostListener('click') 
  onClick() {
    if(!valid) {
      return;
    }
    this.disabled = true;
  }
}

and use it like

<button type="submit" [valid]="!form.valid">Submit<button>

You need to add it to the directives: [PreventDoubleSubmit] of the components where you want to use it or alternatively provide it globally

provide(PLATFORM_DIRECTIVES, {useValue: [PreventDoubleSubmit], multi: true})
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • You're right, for this example it would work. But what if that button would have something like `[disabled]="!form.valid"`? In this case the button would remain enabled. – Daskus May 09 '16 at 17:04
  • 2
    Yup, it's not the most beautyful solution. I just tried to demonstrate ways where you don't need a variable in the component class. maybe I misinterpreted your question. You also could create a directive that implements that feature. I'll update my answer with that approach. – Günter Zöchbauer May 09 '16 at 17:20
  • I added a directive that might do what you want (not tested) – Günter Zöchbauer May 09 '16 at 17:25
  • @GünterZöchbauer I've implemented this globally but the actions attached to the form don't perform. Is there something that needs to be added to the directive to permit the form to continue? – Andrew Howard May 15 '17 at 14:15
  • @AndrewJuniorHoward its long back I know .... Just trying same. were you able to make form submit working with this directive ? I am also facing similar issue and wonder why form submit not working. – ramesh sharma Jul 04 '20 at 10:52
  • @rameshsharma in the end I made my own directive. Please see = https://stackoverflow.com/questions/47636879/disable-buttons-during-ajax-requests/47651379#47651379 – Andrew Howard Jul 04 '20 at 15:35
4

I have a slight different way (maybe more simple) of dealing with this - same principles apply though.

Essentially what I will do is:

  • Create a variable called disableButton on the component with an initial value of false
  • Then when the button is clicked set this variable to true
  • Then reset to false once the form is submitted.

Here is my html template code:

<form #form="ngForm" (ngSubmit)="handleSubmit(form.value, form.valid)">
   <button type="submit" [disabled]="form.invalid || disableButton">
      Submit
   </button>
</form>

And here is my class:

export class UpdateForm {
   disableButton: boolean;
   constructor() { this.disableButton = false; }
   handleSubmit(formData: any, isValid: boolean) {
      if (isValid) {
         this.disableButton = true; // the button will then be disabled
         onHandleUpdate(formData);
      }
   }
   onHandleUpdate(formData) {
      this.disableButton = false; // the button will renable
   }
}
rhysclay
  • 1,645
  • 3
  • 22
  • 42
3

As you are already doing disableButton = true in submit call, you can do check disableButton before calling submit method.

Template

<form (submit)="!disableButton && submit()" >
    <--! Some Inputs -->
    <button [disabled]="disableButton" type="submit">Submit<button>
</form>
Pankaj Parkar
  • 134,766
  • 23
  • 234
  • 299
  • 2
    What would be the profit of checking it before calling submit method? If `disableButton` is `true`, `(submit)` cannot be called anyways because the button is disabled, or did I missunderstand something? – Daskus May 09 '16 at 17:27
0

You could have a base component exposes a get which tells if the component is busy. This get can be used on the template to then disable or enable the button. For something which does an async call the base component has a protected method which takes in a function call. e.g.

Base Component

export abstract class BusyComponent {
    private _isBusy = false;

    get isBusy(): boolean {
        return this._isBusy;
    }

    protected async performBusyTask<T>(busyFunction: () => Promise<T>) {
        this._isBusy = true;
        try {
            return await busyFunction();
        } finally {
            this._isBusy = false;
        }
    }
}

Child Component

class BusyComponentChild extends BusyComponent {

    constructor(private dependency: Dependency) {
        super();
    }

    doSomethingAsync(): Promise<number> {
        return this.performBusyTask<number>(() => this.dependency.doSomethingAsync());
    }
}

Template

<button type="submit" [disabled]="isBusy || !form.valid">Save</button>
0

If you face the double submitting, check if you do not have 2 calls to the submit method in your template. For instance it might be in your <form> as well as in the submitting <button> tags:

<form [formGroup]="form" (ngSubmit)="onSubmit()">

    // very long form content

    <button type="submit" (click)="onSubmit()">Submit</button>

</form>
mpro
  • 14,302
  • 5
  • 28
  • 43