8
\$\begingroup\$

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;
  }
}
\$\endgroup\$
  • \$\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 Aug 27 '18 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 Sep 10 '18 at 9:47
5
\$\begingroup\$

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.

\$\endgroup\$
  • \$\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 Oct 12 at 9:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.