I'm trying to create a multiselect dropdown list with checkbox and filter option. I'm trying to get the list hidden with I click outside but could not figure it out how. Appreciate your help.
9 Answers
Watch out, your solution (the Plunker provided in the question) doesn't close the popups of other boxes when opening a second popup (on a page with multiple selects).
By clicking on a box to open a new popup the click event will always be stopped. The event will never reach any other opened popup (to close them).
I solved this by removing the event.stopPropagation();
line and matching all child elements of the popup.
The popup will only be closed, if the events element doesn't match any child elements of the popup.
I changed the directive code to the following:
select.html (directive code)
link: function(scope, element, attr){
scope.isPopupVisible = false;
scope.toggleSelect = function(){
scope.isPopupVisible = !scope.isPopupVisible;
}
$(document).bind('click', function(event){
var isClickedElementChildOfPopup = element
.find(event.target)
.length > 0;
if (isClickedElementChildOfPopup)
return;
scope.$apply(function(){
scope.isPopupVisible = false;
});
});
}
I forked your plunker and applied the changes:
Plunker: Hide popup div on click outside
Screenshot:
-
16**Performance hint:** If you have allot of these select boxes on your page, you should only bind `click` when the popup is _opened_ and unbind `click` as soon as the popup is _closed_. – cheneym Jun 28 '13 at 13:31
-
cheneym, but what about IE8? I get 'event.target.tagName' is null or not an object – Oct 08 '13 at 13:09
-
oh, I resolve this problem :) In function, which binding on click event, missing input parameter event. I add it, and your solution excellent working in firefox and IE8 – Oct 08 '13 at 13:55
-
@ArtsiomMitrokhin Thanks! I updated the example and the Plunker (I don't know why your edit was rejected). – cheneym Oct 21 '13 at 07:06
-
1It seems like elementMatchesAnyInArray is just trying to see if the event.target is a child of element. Couldn't the same be achieved by checking for `element.find(event.target).length > 0`? – dubilla Nov 04 '13 at 21:11
-
1@dubilla thanks, you are right. I updated the example and the plunker. – cheneym Dec 20 '13 at 13:45
-
2How to do this without jQuery? Without jQuery, element.find is limited to just tag name. – Bhoomtawath Plinsut Oct 24 '14 at 16:57
-
3@BhoomtawathPlinsut `element[0].contains(event.target)`. https://developer.mozilla.org/en-US/docs/Web/API/Node/contains – MathieuLescure Apr 21 '15 at 19:50
-
Try to remove jQuery dependency. – Damjan Pavlica Feb 01 '16 at 14:21
-
Y like the solution of @MathieuLescure without jQuery, only AngularJS. – Francisco Jul 07 '16 at 18:26
This is an old post but in case this helps anyone here is a working example of click outside that doesn't rely on anything but angular.
module('clickOutside', []).directive('clickOutside', function ($document) {
return {
restrict: 'A',
scope: {
clickOutside: '&'
},
link: function (scope, el, attr) {
$document.on('click', function (e) {
if (el !== e.target && !el[0].contains(e.target)) {
scope.$apply(function () {
scope.$eval(scope.clickOutside);
});
}
});
}
}
});

- 914
- 10
- 12
-
1Nice solution although seems to fall short if you have a span or hyperlink to calls a popup but doesn't actually wrap it. You have to apply the attribute to the clickable element which then becomes the target... or it searches to see if it contains the target, but since the hyperlink doesn't wrap the popup, it is unable to prevent the popup from disappearing when clicking inside it. – KingOfHypocrites Jul 20 '15 at 21:05
-
however just wrapping both my link and the popup with a div works great... the simplest answer to this very common problem I could find that worked in a pure angular fashion and particularly great is I don't have to bring in some entire bootstrap coupled library to do it. – KingOfHypocrites Jul 20 '15 at 21:45
-
8
-
1
-
4Works like a charm. But, I suggest to change the `scope.$apply`, and `scope.$eval` to `scope.applyAsync`and `scope.evalAsync`, for a greater performance. – wmarquardt Apr 26 '18 at 21:54
OK I had to call $apply() as the event is happening outside angular world (as per doc).
element.bind('click', function(event) {
event.stopPropagation();
});
$document.bind('click', function(){
scope.isVisible = false;
scope.$apply();
});

- 5,013
- 9
- 37
- 50
-
Really thanks, the first bind solve my problem. using that one when i click on element(dropdown directive) it does not disappear but disappear dropdown when i click outside. – Muhammad Zeshan Ghafoor Oct 05 '16 at 07:41
I realized it by listening for a global click event like so:
.directive('globalEvents', ['News', function(News) {
// Used for global events
return function(scope, element) {
// Listens for a mouse click
// Need to close drop down menus
element.bind('click', function(e) {
News.setClick(e.target);
});
}
}])
The event itself is then broadcasted via a News service
angular.factory('News', ['$rootScope', function($rootScope) {
var news = {};
news.setClick = function( target ) {
this.clickTarget = target;
$rootScope.$broadcast('click');
};
}]);
You can then listen for the broadcast anywhere you need to. Here is an example directive:
.directive('dropdown', ['News', function(News) {
// Drop down menu für the logo button
return {
restrict: 'E',
scope: {},
link: function(scope, element) {
var opened = true;
// Toggles the visibility of the drop down menu
scope.toggle = function() {
element.removeClass(opened ? 'closed' : 'opened');
element.addClass(opened ? 'opened' : 'closed');
};
// Listens for the global click event broad-casted by the News service
scope.$on('click', function() {
if (element.find(News.clickTarget.tagName)[0] !== News.clickTarget) {
scope.toggle(false);
}
});
// Init
scope.toggle();
}
}
}])
I hope it helps!

- 12,481
- 10
- 60
- 72
-
Not so clear how to use this approach. In my directive scope is from the upper controller and doesn't register no clicks for News. Could you please provide some more details or arrange a fiddle for us. Because this solution seems the best for me. Thank you! – Rootical V. Aug 07 '14 at 08:22
I was not totally satisfied with the answers provided so I made my own. Improvements:
- More defensive updating of the scope. Will check to see if a apply/digest is already in progress
- div will also close when the user presses the escape key
- window events are unbound when the div is closed (prevents leaks)
window events are unbound when the scope is destroyed (prevents leaks)
function link(scope, $element, attributes, $window) {
var el = $element[0], $$window = angular.element($window); function onClick(event) { console.log('window clicked'); // might need to polyfill node.contains if (el.contains(event.target)) { console.log('click inside element'); return; } scope.isActive = !scope.isActive; if (!scope.$$phase) { scope.$apply(); } } function onKeyUp(event) { if (event.keyCode !== 27) { return; } console.log('escape pressed'); scope.isActive = false; if (!scope.$$phase) { scope.$apply(); } } function bindCloseHandler() { console.log('binding window click event'); $$window.on('click', onClick); $$window.on('keyup', onKeyUp); } function unbindCloseHandler() { console.log('unbinding window click event'); $$window.off('click', onClick); $$window.off('keyup', onKeyUp); } scope.$watch('isActive', function(newValue, oldValue) { if (newValue) { bindCloseHandler(); } else { unbindCloseHandler(); } }); // prevent leaks - destroy handlers when scope is destroyed scope.$on('$destroy', function() { unbindCloseHandler(); });
}
I get $window
directly into the link function. However, you do not need to do this exactly to get $window
.
function directive($window) {
return {
restrict: 'AE',
link: function(scope, $element, attributes) {
link.call(null, scope, $element, attributes, $window);
}
};
}

- 2,727
- 4
- 24
- 27

- 606
- 5
- 12
There is a cool directive called angular-click-outside
. You can use it in your project. It is super simple to use:

- 5,996
- 4
- 32
- 66
The answer Danny F posted is awesome and nearly complete, but Thịnh's comment is correct, so here is my modified directive to remove the listeners on the $destroy event of the directive:
const ClickModule = angular
.module('clickOutside', [])
.directive('clickOutside', ['$document', function ($document) {
return {
restrict: 'A',
scope: {
clickOutside: '&'
},
link: function (scope, el, attr) {
const handler = function (e) {
if (el !== e.target && !el[0].contains(e.target)) {
scope.$apply(function () {
console.log("hiiii");
// whatever expression you assign to the click-outside attribute gets executed here
// good for closing dropdowns etc
scope.$eval(scope.clickOutside);
});
}
}
$document.on('click', handler);
scope.$on('$destroy', function() {
$document.off('click', handler);
});
}
}
}]);
If you put a log in the handler method, you will still see it fire when an element has been removed from the DOM. Adding my small change is enough to remove it. Not trying to steal anyone's thunder, but this is a fix to an elegant solution.

- 103
- 2
- 7
Installation:
bower install angular-click-outside --save
npm install @iamadamjowett/angular-click-outside
yarn add @iamadamjowett/angular-click-outside
Usage:
angular.module('myApp', ['angular-click-outside'])
//in your html
<div class="menu" click-outside="closeThis">
...
</div>
//And then in your controller
$scope.closeThis = function () {
console.log('closing');
}

- 2,547
- 25
- 16
I found some issues with the implementation in https://github.com/IamAdamJowett/angular-click-outside
If for example the element clicked on is removed from the DOM, the directive above will trigger the logic. That didn't work for me, since I had some logic in a modal that, after click, removed the element with a ng-if.
I rewrote his implementation. Not battle tested, but seems to be working better (at least in my scenario)
angular
.module('sbs.directives')
.directive('clickOutside', ['$document', '$parse', '$timeout', clickOutside]);
const MAX_RECURSIONS = 400;
function clickOutside($document, $parse, $timeout) {
return {
restrict: 'A',
link: function ($scope, elem, attr) {
// postpone linking to next digest to allow for unique id generation
$timeout(() => {
function runLogicIfClickedElementIsOutside(e) {
// check if our element already hidden and abort if so
if (angular.element(elem).hasClass('ng-hide')) {
return;
}
// if there is no click target, no point going on
if (!e || !e.target) {
return;
}
let clickedElementIsOutsideDirectiveRoot = false;
let hasParent = true;
let recursions = 0;
let compareNode = elem[0].parentNode;
while (
!clickedElementIsOutsideDirectiveRoot &&
hasParent &&
recursions < MAX_RECURSIONS
) {
if (e.target === compareNode) {
clickedElementIsOutsideDirectiveRoot = true;
}
compareNode = compareNode.parentNode;
hasParent = Boolean(compareNode);
recursions++; // just in case to avoid eternal loop
}
if (clickedElementIsOutsideDirectiveRoot) {
$timeout(function () {
const fn = $parse(attr['clickOutside']);
fn($scope, { event: e });
});
}
}
// if the devices has a touchscreen, listen for this event
if (_hasTouch()) {
$document.on('touchstart', function () {
setTimeout(runLogicIfClickedElementIsOutside);
});
}
// still listen for the click event even if there is touch to cater for touchscreen laptops
$document.on('click', runLogicIfClickedElementIsOutside);
// when the scope is destroyed, clean up the documents event handlers as we don't want it hanging around
$scope.$on('$destroy', function () {
if (_hasTouch()) {
$document.off('touchstart', runLogicIfClickedElementIsOutside);
}
$document.off('click', runLogicIfClickedElementIsOutside);
});
});
},
};
}
function _hasTouch() {
// works on most browsers, IE10/11 and Surface
return 'ontouchstart' in window || navigator.maxTouchPoints;
}