I have written an Angular Directive that disables a button after it has been clicked, to prevent double posting / saving of data. This works fine in most of our use cases. However, in some situation we want to re-enable the button. For example: the button-click triggers a form submit, but the server-side validation fails. We want to allow the user to submit again, so we need to re-enable the button.
My solution uses an EventEmitter
that is owned by the 'host' Component. This EventEmitter
is passed on to the button's Directive. This Directive then subscribes to it. Whenever the host wants to instruct the button to re-enable, is emits an event. The Directive will take care of enabling the button.
As an alternative, one could use the disabled
property of the button to control its state. However, I would like to reserve that property for other logic that is part of the host Component (e.g. are all required fields correctly filled, etc.). I would like to keep the code for the double posting safeguard contained to the Directive as much as possible.
I'd like to learn if there might be a cleaner way to accomplish what I'm after. Here's what I do now:
The button as used in the host Component template
<button [appDisableAfterClick]="reenableButton"
(click)="doStuff()">My button</button>
Inside the host Component
private reenableButton = new EventEmitter<boolean>();
doStuff() {
// Make some external call.
// In case of an error, re-enable the button:
this.reenableButton.emit();
}
The DisableAfterClick
Directive
Several null checks and unsubscribe calls omitted for brevity.
@Directive({
selector: '[appDisableAfterClick]'
})
export class DisableAfterClickDirective implements OnChanges, OnDestroy {
@Input('appDisableAfterClick') reenableButton: EventEmitter<boolean>;
ngOnChanges(changes: SimpleChanges) {
this.reenableButton = changes.reenableButton.currentValue;
this.reenableButton.subscribe(_ => {
(<HTMLButtonElement>this.el.nativeElement).disabled = false;
});
}
@HostListener('click')
onClick() {
(<HTMLButtonElement>this.el.nativeElement).disabled = true;
}
}
-
\$\begingroup\$ Why exactly are you using a directive though? Is this a directive that will be used in several parts of your application? \$\endgroup\$SiddAjmera– SiddAjmera2018年08月27日 13:23:50 +00:00Commented Aug 27, 2018 at 13:23
-
\$\begingroup\$ Yes, the application contains several screens, most of which have a Save-button that needs this functionality. The directive makes it easy to add this kind of behavior to any button that requires it, with minimal code duplication. \$\endgroup\$Daan– Daan2018年09月10日 09:47:19 +00:00Commented Sep 10, 2018 at 9:47
1 Answer 1
One of the main ways in which you can improve this is by using Renderer2 in the directive instead of directly accessing this.el.nativeElement
and making changes to it. This might work in most of the cases. But remember, the Angular code might run at other places as well(like in case of Service Workers and in case of Server Side Rendering). In these cases, you might not have direct access to the this.el.nativeElement
.
So it's always recommended to use Renderer2
instead. Once you start using the renderer, your directive logic would significantly reduce. Here's how:
import { Directive, OnDestroy, Input, EventEmitter, HostListener, Renderer2, ElementRef } from '@angular/core';
import { Subscription } from 'rxjs';
@Directive({
selector: '[appDisableAfterClick]'
})
export class DisableAfterClickDirective {
@Input('appDisableAfterClick') reenableButton: EventEmitter<boolean>;
subscription: Subscription;
constructor(
private renderer: Renderer2,
private el: ElementRef
) {}
@HostListener('click')
onClick() {
this.renderer.setAttribute(this.el.nativeElement, 'disabled', 'true');
}
ngOnInit() {
this.subscription = this.reenableButton.subscribe(value => {
if(!value) this.renderer.removeAttribute(this.el.nativeElement, 'disabled');
});
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
}
Another thing would be to not have this directive itself in the first place unless you are going to reuse it in several other places in your App.
I've created this StackBlitz for your reference. Just in case.
-
\$\begingroup\$ That's very good, specially if you're changing classes, but for the disabled attribute, I think using the host binding decorator would be easier to read/work with. Example:
@HostBinding('disabled') isDisabled: boolean;
\$\endgroup\$eestein– eestein2019年10月12日 09:23:01 +00:00Commented Oct 12, 2019 at 9:23
Explore related questions
See similar questions with these tags.