How to close Angular-bootstrap popover when clicking outside

20,211

Solution 1

First of all, if you want the popover to close on any click, not only the one outside of your popover, you can do it using existing UI-Bootstrap code:

<button class="btn btn-default btn-xs" type="button"
        popover-template="level.changeLevelTemplate"
        popover-trigger="focus"
        popover-placement="right">
  <span class="glyphicon glyphicon-sort"></span>
</button>

The trick here is to drop the surrounding <div> and put the popover-trigger="focus" right on the button.


If you need to actually close the popover only for clicks outside the popover content, then it will be more difficult. You need a new directive, like this one:

app.directive('clickOutside', function ($parse, $timeout) {
  return {
    link: function (scope, element, attrs) {
      function handler(event) {
        if(!$(event.target).closest(element).length) {
          scope.$apply(function () {
            $parse(attrs.clickOutside)(scope);
          });
        }
      }

      $timeout(function () {
        // Timeout is to prevent the click handler from immediately
        // firing upon opening the popover.
        $(document).on("click", handler);
      });
      scope.$on("$destroy", function () {
        $(document).off("click", handler);
      });
    }
  }
});

Then, in your popover template, use the directive on the outermost element:

<div click-outside="level.closePopover()">
   ... (actual popover content goes here)
</div>

Finally, in your controller, implement the closePopover function:

vm.closePopover = function () {
  vm.togglePopover = false;
};

What we've done here is:

  • we're listening on any clicks on the document, and, if the click is outside of the element to which we added our close-popover directive:
    • we invoke whatever code was the value of close-popover
  • we also clean up after ourselves when the directive's scope is destroyed (i.e. when the popover is closed) so that we don't handle the clicks anymore.

It's not the cleanest solution, as you have to invoke the controller method from within the popover template, but it's the best I came up with.

Solution 2

Since angular-ui 1.0.0, there is a new outsideClick trigger for tooltips and popovers (introduced in this pull request:

<div
  uib-popover-template="level.changeLevelTemplate"
  popover-trigger="outsideClick"
  popover-placement="right">
  <button class="btn btn-default btn-xs" type="button">
    <span class="glyphicon glyphicon-sort"></span>
  </button>
</div>

Solution 3

If I understood correctly, you want the popover to close when a user clicks pretty much anywhere except on the inside of the popover itself, barring the actual close button. This could be accomplished with an event listener:

$('html').click(function() {
    if(!$(event.target).is('#foo')) {
        // Code to hide/remove popovers
    }
});

Check out this plunkr.

Or, in your specific scenario:

$('html').click(function() {
    if(!$(event.target).is('.my-popover-class')) {
        vm.togglePopover = false;
    }
})

Solution 4

close the popover when clicking anywhere outside of the popover

Some time ago I've found this answer useful: How to dismiss a Twitter Bootstrap popover by clicking outside?

Code I used in one of my demos (mixing angular and jQueryevent handling which is probably not recommended) is specific to my needs but may give some idea:

  app.directive("eventlistener", function($rootScope) {
    $(window).resize($rootScope.closeAllPopovers); // because Bootstrap popovers don't look good when misplaced

    return {
      link: function(scope, element, attrs) {
        $('body').on('mouseup touchend', $rootScope.closeAllPopovers);
      }
    };
  });

  $rootScope.closeAllPopovers = function (e) {
    $('[data-toggle="popover"]').each(function () {
      if (e) {
        if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) {
          $(this).popover('hide');
        }
      } else {
        // No event passed - closing all popovers programmatically
        $(this).popover('hide');
      }
    });
  };

I'd also suggest looking at the difference between:

Solution 5

You're going to need to do the event handling yourself as when you use the new *-is-open attributes, there is no event handling.

If you don't need the programmatic control over opening/closing the popover, then you can use the built in focus trigger to give you what you want.

Share:
20,211
MattDionis
Author by

MattDionis

Experienced Full-Stack JavaScript Engineer &amp; Engineering Manager with a passion for GraphQL and React!

Updated on July 09, 2022

Comments

  • MattDionis
    MattDionis almost 2 years

    I am attempting to close my Angular-bootstrap popovers when clicking anywhere outside the popovers. According to an answer to this question this can now be accomplished (in version 0.13.4) by utilizing the new popover-is-open attribute: Hide Angular UI Bootstrap popover when clicking outside of it

    Currently my HTML looks like so:

    <div
      ng-click="level.openTogglePopover()"
      popover-template="level.changeLevelTemplate"
      popover-trigger="none"
      popover-placement="right"
      popover-is-open="level.togglePopover">
      <button class="btn btn-default btn-xs" type="button">
        <span class="glyphicon glyphicon-sort"></span>
      </button>
    </div>
    

    ...and my relevant controller code:

    vm.togglePopover = false;
    
    vm.openTogglePopover = function() {
      vm.togglePopover = !vm.togglePopover;
    };
    

    This works great for opening/closing the popover when clicking on the button referenced above. My question is, how would I extend this functionality to close the popover when clicking anywhere outside of the popover? How would I set up my event handling to accomplish this?

  • MattDionis
    MattDionis over 8 years
    I've changed my trigger to popover-trigger="focus", but the popovers do not even open now.
  • MattDionis
    MattDionis over 8 years
    Thank you very much. I'm very close now, but my popover closes when I click inside it still. This is because I'm not using jQuery so I need to tweak some of the link function code. Any idea how this line could be written without using jQuery: if(!$(event.target).closest(element).length)?
  • flashrocks
    flashrocks over 8 years
    I think you can emulate this by e.g. if (!element.contains(event.target)) {...
  • MattDionis
    MattDionis over 8 years
    That's what I've tried, but I'm getting an element.contains is not a function error.
  • flashrocks
    flashrocks over 8 years
    Strange, element should always be a Node... Can you debug and check what type it is exactly?
  • MattDionis
    MattDionis over 8 years
    Ended up checking to see if the element contains the HTML of the target: if (element.context.outerHTML.indexOf($event.target.outerHTML) === -1).
  • flashrocks
    flashrocks over 8 years
    Don't do that, it's very ineffective. I just remembered: element is a jqLite instance, you need to call element[0] to get the DOM element. So your if statement would be: if (!element[0].contains(event.target)) {...
  • Yogesh Sanchihar
    Yogesh Sanchihar about 6 years
    superb. popover-trigger is what I needed.