Search code examples
angulares6-promiseangular-directive

Angular directive that waits for click Promise to complete


Update 1: Based on answer from @PierreDuc, I have forked and created a simpler version here: Simplified Example. Man, I remember in AngularJS being able to totally hijack the (click) function and execute it. I get it, I get it, I like the simplified proposal below though. That should work for me :)

I have created an Angular button directive that accepts a method as a Promise.
On click, I disable the button. When the Promise is resolved I then re-enables the button.

Basically, on click I want to disable the button until all processing that button event is performing completes, including any http calls.

I have accomplished my goal seen here: Stackblitz Example.

I don't particularly like my "solution".

It's overly complicated. In order to get it to work three things need to happen.

  1. The directive be added to the button.
  2. The "waitFor" property be set.
  3. The "waitFor" function must be a function that returns a Promise.

IMO, that's too many things that need to align to get it work.

What I would really like to do is get a handle on the (click) method of the button and execute it manually in the directive the way I did my "waitFor" property.

How can I do this?

At the very least, I would like a directive that doesn't require both the directive name ("appClickWait") and the property ("[waitFor]").

Here is the code for your convenience:

Directive:

import { Directive, HostListener, ElementRef, Output, Input, EventEmitter, Renderer2 } from '@angular/core';

// Enfore this directvie can only be used on a button?
@Directive({
  selector: '[appClickWait]'
})
export class ClickWaitDirective {
  @Input("waitFor") clickWait: any;

  constructor(private el: ElementRef, private renderer: Renderer2) { }

  @HostListener('click', ['$event'])
  clickEvent(event) {
    event.preventDefault();
    event.stopPropagation();

    this.renderer.setAttribute(this.el.nativeElement, 'disabled', 'disabled');

    const originalInnerText = this.el.nativeElement.innerText;

    this.el.nativeElement.innerText = 'Processing...';

    const reset = () => {
      this.renderer.removeAttribute(this.el.nativeElement, 'disabled');

      this.el.nativeElement.innerText = originalInnerText;
    };

    // I really would like to just get a handle on the (click) function here
    // that would greatly simplify the useage of this directive
    this.clickWait()
      .then((data) => {
        reset();
      })
      .catch(err => {
        console.error(err);

        reset();
      });
  }
}

Template:

  myClickFunc = async () => {
    console.log('start')

    this.posts = [];

    // too fast, let's wait a bit
    // this.posts = await this.http.get('https://jsonplaceholder.typicode.com/posts').toPromise();

    await new Promise((resolve, reject) => {
      setTimeout(async () => {
        try {
          this.posts = await this.http.get('https://jsonplaceholder.typicode.com/posts').toPromise();
        } catch (err) {
          reject(err);
        }
        resolve();
      }, 1000);
    });

    console.log('all done');
  }
<button type="button" appClickWait [waitFor]="myClickFunc">Single Wait Click</button>

Thank you!


Solution

  • I suppose you can simplify it to this:

    @Directive({
       selector: 'button[appClickWait]'
    })
    export class ClickWaitDirective {
       @HostBinding('disabled')
       public waiting = false;
        
       @Input()
       appClickWait: () => Observable<any> | Promise<any> = async() => void 0;
        
       @HostListener('click')
       clickEvent() {
          this.waiting = true;
        
          from(this.appClickWait()).pipe(take(1)).subscribe({
             subscribe: () => this.waiting = false,
             complete: () => this.waiting = false,
             error: (e) => {
                console.error(e);
                this.waiting = false;
             }
          });
       }
    }
    

    With this usage:

    <button [appClickWait]="myClickFunc">Single Wait Click</button>
    
    myClickFunc = () => this.callHttp();
    

    This way it will only work on a button. The disabled attribute will get set automatically, and you can insert a function which returns a promise or observable.

    stack;