15 thoughts on “Close popover on click outside

  1. For anyone monitoring this issue here is a short write-up summarizing our options:

    1. Do nothing – don’t add new API as things are possible today
    2. Add new Boolean flag (closeOnOutsideClick or similar)
    3. Add new “fake” event (outsideClick ?) so this “fake” event can be used as part of the triggers

    ad.1

    The reason that we didn’t rush to any implementation so far is that the current behavior is aligned with what Bootstrap does and people can decide to have popovers closed on the “outside click” as follows:

    <button type="button" class="btn btn-secondary" placement="top"
            ngbPopover="Vivamus sagittis lacus vel augue laoreet rutrum faucibus." 
            popoverTitle="Popover on top" 
            #p="ngbPopover" (document:click)="p.close()" (click)="$event.stopPropagation()">
      Popover on top
    </button>

    A working plunker: http://plnkr.co/edit/ZVh3vSwb5DBfuA2IFyiY?p=preview

    This is something that works today and is very flexible (people have full control over an “outside” trigger). There are 3 downsides to this approach, though:

    • it is a bit verbose to write #p="ngbPopover" (document:click)="p.close()" (click)="$event.stopPropagation()" for each and every popover instance;
    • we can’t have global configuration to express “close all popovers on outside click, always”
    • people might not known the #p="ngbPopover" (document:click)="p.close()" (click)="$event.stopPropagation()" trick (this could be helped with documentation).

    ad. 2

    Another approach is to simply add a new @Input (outsideClick) so one could write:

    <button type="button" class="btn btn-secondary" placement="top"
            ngbPopover="Vivamus sagittis lacus vel augue laoreet rutrum faucibus." 
            popoverTitle="Popover on top" 
            [outsideClick]="true">
      Popover on top
    </button>

    It is a very simple mechanism (both to implement and use) but has “hard-coded” outside event.

    ad 3.

    Another approach (implemented in http://angular-ui.github.io/bootstrap/#/popover and proposed in #1064) is to express “close on outside click” as part of the triggers @Input() , ex:

    <button type="button" class="btn btn-secondary" placement="top"
            ngbPopover="Vivamus sagittis lacus vel augue laoreet rutrum faucibus." 
            popoverTitle="Popover on top" 
            triggers="outsideClick">
      Popover on top
    </button>

    This seems like a good idea at first but it has many issues:

    • limits flexibility of triggers – what is I want to open a popover on focus, close on blur and still close such popover on outside clicks?
    • has a “hard-coded” event name;
    • it is not possible to express that all popovers should be closed on outside clicks in global configuration.
  2. Given the above analysis I believe that option (3) has too many shortcomings to be part of this library API. (1) is a good work-around till we come up with something better.

    Speaking of “something better” here is my proposal: let’s introduce a new @Input named outsideClose. This new input could take the following values:

    • false – (default) current behavior
    • true – close a popover on outside click
    • "click;tap"; separated event names to be used as “outside triggers”

    If I don’t here any comments / screams / better ideas I’m going to implement the above in the coming day(s).

  3. For those waiting for this to be implemented, here’s what I’m using:

    import { Directive, Input, HostListener } from '@angular/core';
    
    @Directive({
      selector: '[closePopoverOnClickOutside]'
    })
    export class ClosePopoverOnClickOutsideDirective {
    
      active = false;
    
      @Input('closePopoverOnClickOutside') popover: { close, isOpen };
    
      @HostListener('document:click', ['$event.target'])
      docClicked(target): Boolean {
        if (!this.popover.isOpen()) {
          return this.active = false;
        }
        // Click that opens popover triggers this. Ignore first click.
        if (!this.active) {
          return this.active = true;
        }
    
        let cancelClose = false;
        let popoverWindows = document.getElementsByTagName('ngb-popover-window');
    
        for (let i = 0; i < popoverWindows.length; i++) {
          cancelClose = cancelClose || popoverWindows[i].contains(target);
        }
        if (!cancelClose) {
          this.popover.close();
        }
    
        // Deactivate if something else closed popover
        this.active = this.popover.isOpen();
      }
    
    }

    And use thusly:

    <div ngbPopover="Closes when click occurs outside"
         #p="ngbPopover"
         [closePopoverOnClickOutside]="p"></div>
  4. Given that the title of the issue is “Close popover on click outside” and the example closes it whether you click inside or outside the popover, I’m not sure this addresses the issue.

  5. Agree with @spongessuck. The main idea of this issue was to implement closing on clicking outside popover, not outside popover trigger (Bootstrap’s behavior). Many users in this issue pointed out that they want to prevent popover closing when clicking inside it. Example use cases are having some inputs inside the popover and let users interact with those.

  6. @nwp90:

    it should work if it runs outside of angluar change detection, try this:

    import { Directive, OnInit, OnDestroy, ElementRef, ComponentRef, ChangeDetectorRef, NgZone, Renderer2 } from '@angular/core';
    import { NgbPopover } from '@ng-bootstrap/ng-bootstrap';
    import { NgbPopoverWindow } from '@ng-bootstrap/ng-bootstrap/popover/popover';
    
    @Directive({
      selector: '[closePopoverOnOutsideClick][ngbPopover]'
    })
    export class ClosePopoverOnOutsideClickDirective implements OnInit, OnDestroy {
    
      listener: () => void;
    
      constructor(private elementRef: ElementRef, private ngbPopover: NgbPopover,
                  private ngZone: NgZone, private cd: ChangeDetectorRef, private renderer: Renderer2) {
      }
    
      ngOnInit() {
        this.ngZone.runOutsideAngular(() => {
          this.listener = this.renderer.listen('document', 'click', (event) => {
            this.closePopoverOnClickOutside(event);
          });
        });
      }
    
      ngOnDestroy() {
        this.listener();
      }
    
      private closePopoverOnClickOutside(event: MouseEvent): void {
        // Popover is open
        if (this.ngbPopover && this.ngbPopover.isOpen()) {
          // Not clicked on self element
          if (!this.elementRef.nativeElement.contains(event.target)) {
            // Hacking typescript to access private member
            const popoverWindowRef: ComponentRef<NgbPopoverWindow> = (this.ngbPopover as any)._windowRef;
            // If clicked outside popover window
            if (!popoverWindowRef.location.nativeElement.contains(event.target)) {
              this.ngbPopover.close();
              this.cd.detectChanges(); // detect changes
            }
          }
        }
      }
    
    }
  7. Nevermind! I just got this to work by doing the following:

      @ViewChild('msPopover') public popover: NgbPopover;
    
      constructor(private _eref: ElementRef) {}
    
      onClick(event) {
       if (!this._eref.nativeElement.contains(event.target)) // or some similar check
         this.popover.close();
      }
    

    Doing that seems to make the popover close when clicking anywhere outside of the popover and the element that toggled it while still allowing clicks inside to keep the popover open.

    EDIT:
    You also need to add the following to the @component:

    @Component({
      ...
      host: {'(document:click)': 'onClick($event)'},
    })
    
  8. @bluecaret:

    usage is like this:

    <ng-template #popoverTemplate>
      this is the content of the popover
    </ng-template>
    
    
    <a [ngbPopover]="popoverTemplate" placement="top" closePopoverOnOutsideClick>
      open popover
    </a>
  9. This issue is still opened. More then one year. It is incredible! It is one of the most important features, but I see all thread of excuses and temporary decisions like #p="ngbPopover" (document:click)="p.close()" which will not even work with *ngFor-generated elements.

    The more I use ng-bootstrap the better I understand that you, guys haven’t enough real practice and you make components in abstract vacuum. It is OK that you develop theory of creating Angular components, but I still want to use your components in work.

    Please, implement all features, which users need here and implement missing features from ngx-bootstrap, angularStrap etc. libraries. Do it quickly with hardcode, shitcode, bad patterns… it does not matter. After that you will understand what should be the components and can improve your code and API in v. 2.0. Because now you just waste time try to create the ideal not knowing how it should look like

  10. If you’re trying to allow one popover at a time (dismissible by clicking outside), you could achieve that by using the last opened popover reference and the @HostListener decorator like this:

    lastPopoverRef: any;
    
    @HostListener('document:click', ['$event'])
    clickOutside(event) {
      // If there's a last element-reference AND the click-event target is outside this element
      if (this.lastPopoverRef && !this.lastPopoverRef._elementRef.nativeElement.contains(event.target)) {
        this.lastPopoverRef.close();
        this.lastPopoverRef = null;
      }
    }
      
    setCurrentPopoverOpen(popReference) {
      // If there's a last element-reference AND the new reference is different
      if (this.lastPopoverRef && this.lastPopoverRef !== popReference) {
        this.lastPopoverRef.close();
      }
      // Registering new popover ref
      this.lastPopoverRef = popReference; 
    }
    
    

    Then, use it like this:

    <button [ngbPopover]="popoverContent" 
            #popoverRef="ngbPopover" 
            (click)="setCurrentPopoverOpen(popoverRef)">
    </button>
    
  11. Wow! More than a year now. Any good enterprise application would need this feature! Someone rightly said forget about figuring out ideal implementation after all the landscape may be completely different 2-3 years down the road.

    I believe the 3rd option having outsideClick option in the triggers is the best way to go about this.

  12. @shyamal890 I actually wrote a generic, simple solution which handles all popup widgets and some cases such as nested popups (dropdown in a dialog for instance). I am doing this as part of my job and unfortunately had other tasks to work on in the meantime. What’s remaining is the testing part (which needs to be changed completely), and then validation from integrators.

    Hopefully I will open the PR next week. Sorry for adding more delay to this.

  13. For anyone tracking this issue: we’ve got a PR in-flight (#2554) that will bring native support for closing on outside clicks. It should land as part of the next feature release (early next week).