4

I'm using a tooltip directive , so when I click a <button> - I dynamically inject and display a tooltip.

It does work well and I do see the tooltips :

enter image description here

This is the code I've used to inject a tooltip when clicking a button :

@Directive({ selector: '[popover]'})
class Popover {
  private _component: ComponentRef<>;
    constructor(private _vcRef: ViewContainerRef, private _cfResolver: ComponentFactoryResolver,private elementRef: ElementRef) {
    }
    @HostListener('click')
  toggle() {
    if (!this._component) {
      const componentFactory = this._cfResolver.resolveComponentFactory(PopoverWindow);
      this._component = this._vcRef.createComponent(componentFactory);

    } else {
      this._vcRef.clear()
      this._component.destroy();
      this._component = null;
    }
  }
} 

But I don't want more than one tooltip to appear in the screen.
In other words , before I inject a tooltip - I want to remove all existing tooltips - if any.

Question:

How can I "find" all existing tooltips and remove them before I insert a new one ?

I know that I can add a class to each one and then remove them via removeNode but I want to do it in the Angular way.

Full Plunker

BTW - I'll be happy to find a general solution also for components and not only for directives. ( If possible).

Royi Namir
  • 144,742
  • 138
  • 468
  • 792
  • 2
    https://plnkr.co/edit/NU2FKlO6Q66bRde9m9AC?p=preview Here is one more approach https://medium.com/@amcdnl/building-tooltips-for-angular2-396320fa938f Check also this question https://stackoverflow.com/questions/42598169/add-a-component-dynamically-to-a-child-element-using-a-directive. – yurzui Jun 01 '17 at 12:44
  • 1
    :-) @yurzui As always , thank you . – Royi Namir Jun 01 '17 at 12:46
  • @yurzui Can you please have a look [here](https://stackoverflow.com/questions/44530654/angular-service-injecting-dynamic-component) ? – Royi Namir Jun 13 '17 at 20:31

1 Answers1

3

An obvious right answer would be to use a service and have your popup inject this service and register to it when they open and close, to know if there is a current popup open.

But lets look at another, less obvious solution.. Which can be frowned upon, but for tiny things like this, really seems to be the easiest and readable way. To use a static property on your Popover class:

@Directive({ selector: '[popover]'})
class Popover {
  private static currentPopover: Popover;

  private get active() {
      return this === Popover.currentPopover;
  } 

  private component: ComponentRef<any>;

  constructor(
       private vcRef: ViewContainerRef, 
       private cfResolver: ComponentFactoryResolver,
       private elementRef: ElementRef
  ) {}

  @HostListener('document:click')
  onDocClick() {
    if (this.active) {
      this.close();
    }
  }

  @HostListener('click', ['$event'])
  toggle(event: MouseEvent) {
    if (Popover.currentPopover && !this.active) {
      Popover.currentPopover.close();
    } 
    if (!this.active) {
      this.open();
      event.stopImmediatePropagation();
    }
  }

  open() {
    const componentFactory = this.cfResolver.resolveComponentFactory(PopoverWindow);
    this.component = this.vcRef.createComponent(componentFactory);
    Popover.currentPopover = this;
  }

  close() {
    this.vcRef.clear()
    this.component.destroy();
    this.component = null;
    Popover.currentPopover = undefined;
  }
} 

I've also added a document click listener, so when you click anywhere else, it closes the current popup.

plunkr

But if you are willing to use a service (untested code):

export class PopoverService {

    private activePopover: Popover;

    public setActive(popover: Popover): void {
        if (this.activePopover) {
            this.activePopover.close();
        }
        this.activePopover = popover;
    }   

    public isActive(popover: Popover): boolean {
       return popover === this.activePopover;
    }
}

And your directive will look like this:

@Directive({ selector: '[popover]'})
class Popover {

  private get active() {
      return this.popoverService.isActive(this);
  } 

  private component: ComponentRef<any>;

  constructor(
       private vcRef: ViewContainerRef, 
       private cfResolver: ComponentFactoryResolver,
       private elementRef: ElementRef,
       private popoverService: PopoverService
  ) {}

  @HostListener('document:click')
  onDocClick() {
    if (this.active) {
       this.close();
    }
  }

  @HostListener('click', ['$event'])
  toggle(event: MouseEvent) {
    if (!this.active) {
       this.open();           
       event.stopImmediatePropagation();
    }
  }

  open() {
    const componentFactory = this.cfResolver.resolveComponentFactory(PopoverWindow);
    this.component = this.vcRef.createComponent(componentFactory);
    this.popoverService.setActive(this);
  }

  close() {
    this.vcRef.clear()
    this.component.destroy();
    this.component = null;
  }
}
Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
  • Regarding the "right approach" - do you mean a top level ( singleton) injected service that holds a reference to each visible popup which listens to "closeAll()" event ? and then iterate through the array and invoke `closeMe()` at each directive ? – Royi Namir Jun 01 '17 at 12:35
  • Sorta. But you don't need a `closeAll`, because there is only one active. You should let the component talk to the service saying.. hiiii im the active one now.. and from the service, close the previous (if there is), and set the one saying hiii the active one – Poul Kruijt Jun 01 '17 at 12:40
  • Service cannot _close_ nothing. it jsut publish (next()) an event via a Subject with the selected ref ( the active one as you say) , and at each directive's subscribtion - it checks if the ref is `this` and if not - it remove itself. Right ? – Royi Namir Jun 01 '17 at 12:43
  • 1
    Ill add a 'solution' using a service. Hold on :) – Poul Kruijt Jun 01 '17 at 13:01