2

I have a clickaway listener as a directive that uses @HostListener put on App.component.ts

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
})
export class AppComponent {
  constructor(private clickaway: ClickawayService) {}

  @HostListener("document:click", ["$event"]) documentClick(event: any): void {
    this.clickaway.target.next(event.target);
  }
}

@Directive({
  selector: "[clickaway]",
})
export class ClickawayDirective implements OnInit, OnDestroy {
  @Input() clickaway = null;

  private subscription: Subscription = null;
  constructor(
    private clickawayService: ClickawayService,
    private eleRef: ElementRef
  ) {}

  ngOnInit() {
    this.subscription = this.clickawayService.target.subscribe((target) => {
      if (!this.eleRef.nativeElement.contains(target)) {
        this.clickaway();
      }
    });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

The ClickAway service just provides a subject to listen to document:click on the AppComponent.

I am using this directive on a div that has child controlled by *ngIf, something like:

<div [clickaway]="doSomething">
  <span *ngIf="isVisible">
    <button (click)="closeSpan()">close</button> //closeSpan() sets isVisible to false.
  </span>
</div>

The problem is whenever I click on close, it also triggers the clickaway's doSomething function.

I understand that *ngIf removes the span from dom thus when the directive runs !this.eleRef.nativeElement.contains(target) evaluates to false, since element is not there.

What I have tried so far is:

closeSpan() {
  setTimeout(() => this.isVisible = false, 100);
}

and moving the span out of view using position: absolute and very high offsets and removing *ngIf.

These solutions work, but I am seeking a more elegant way, preferably the directive itself handling such cases.

Thanks

Nilesh Kumar
  • 343
  • 1
  • 5
  • 18

3 Answers3

0

In practice, setTimeout() tells the browser to invoke the function when it has finished running the event handlers for any currently pending events and has finished updating the current state of the document. source

Try setting the delay to 0 for a more elegant solution. Calling setTimeout with a delay of 0 (zero) milliseconds doesn't execute the callback function after the given interval. The execution depends on the number of waiting tasks in the queue. source

Your problem is interesting and requires some nip and tuck. I believe you already have the answer. Since you want to do 2 seperate things with the click event, you need to time them according to your preference.

Other possible solutions include using a boolean variable isHandled: boolean in your service to avoid an execution. Or something like pushing the current clicked target (not the document clicked target) to your service and do additional checks.

ginalx
  • 1,905
  • 1
  • 15
  • 19
  • Thanks for looking into it. I have tried finding a more elegant solution, but I guess there is none. Though I am leaving this question open for someone having a better understanding of it. – Nilesh Kumar Jun 29 '20 at 17:30
  • I am sorry I could not come up with something better. Your problem is similar to https://stackoverflow.com/a/46656671/7322763. You must either use flags or the setTimeout. I would be happy if the timeout works with 0 delay but I would also accept a flag solution. – ginalx Jun 29 '20 at 19:03
0

I'm not familiar with click away, but would something like this work?

<div [clickaway]="doSomething()"> 
  <button *ngIf="isVisible" (click)="isVisible=false">close</button>
</div>

doSomething() {
  if(this.isVisible) {
  }
}
Rick
  • 1,710
  • 8
  • 17
  • I have simplified my problem in the above example, actually I am rendering a chain of components within the div, and this button is analogous to an element inside the component, so I might not be able to get a reference `isVisible` inside doSomething and handling it for each such element seems complex. – Nilesh Kumar Jun 29 '20 at 17:25
  • since you are using angular, I can't think of any reason to ever use document:click. why not just use (click)="" ? – Rick Jun 29 '20 at 17:34
  • `document:click` is for clickaway directive, it listens to any click on application and calls a `subject.next` with event data the `clickaway` directive subscribes to this subject and when a click event occurs the directive checks if the clicked element is the child of the host element or not, which tells it, if the click was inside or outside. – Nilesh Kumar Jun 29 '20 at 17:38
  • This is what I have implemented for clickaway using @ginalx's answer to some other question. If you have some other approach for this I can try it. – Nilesh Kumar Jun 29 '20 at 17:40
0

Try to change the *ngIf:

  <span *ngIf="isVisible">
    <button (click)="closeSpan()">close</button> //closeSpan() sets isVisible to false.
  </span>

For hidden:

  <span [hidden]="!isVisible">
    <button (click)="closeSpan()">close</button> //closeSpan() sets isVisible to false.
  </span>