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
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})
I have a slight different way (maybe more simple) of dealing with this - same principles apply though.
Essentially what I will do is:
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
}
}
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.
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>
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.
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;
}
}
}
class BusyComponentChild extends BusyComponent {
constructor(private dependency: Dependency) {
super();
}
doSomethingAsync(): Promise<number> {
return this.performBusyTask<number>(() => this.dependency.doSomethingAsync());
}
}
<button type="submit" [disabled]="isBusy || !form.valid">Save</button>
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>