The dropdown directive binds the click event on the document, but when you click on the row, the event starts propagating from the target element down to the root document node (td
-> tr
-> table
-> document
).
So that's why your ng-click
handler, that you have on your row, always gets called, even though the directive is "stopping" the bubble on document click.
Solution is to use the useCapture flag when adding the click handler for the document.
After initiating capture, all events of the specified type will be
dispatched to the registered listener before being dispatched to any
EventTarget beneath it in the DOM tree. mdn
Now, to instruct the dropdown directive to use your own handler, you need to change the source of the directive. But it's a third party directive, and you probably don't want to do that, for maintability reasons.
This is where the powerful angular $decorator kicks in. You may use the $decorator to change the source of the third-party module on-the-fly, without actually touching the actual source files.
So, with decorator in place, and with custom event handler on the document node, this is how you can make that dropdown behave:
FIDDLE
var myApp = angular.module('myApp', []);
/**
* Original dropdownToggle directive from ui-bootstrap.
* Nothing changed here.
*/
myApp.directive('dropdownToggle', ['$document', '$location', function ($document, $location) {
var openElement = null,
closeMenu = angular.noop;
return {
restrict: 'CA',
link: function(scope, element, attrs) {
scope.$watch('$location.path', function() { closeMenu(); });
element.parent().bind('click', function() { closeMenu(); });
element.bind('click', function (event) {
var elementWasOpen = (element === openElement);
event.preventDefault();
event.stopPropagation();
if (!!openElement) {
closeMenu();
}
if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
element.parent().addClass('open');
openElement = element;
closeMenu = function (event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
$document.unbind('click', closeMenu);
element.parent().removeClass('open');
closeMenu = angular.noop;
openElement = null;
};
$document.bind('click', closeMenu); /* <--- CAUSE OF ALL PROBLEMS ----- */
}
});
}
};
}]);
/**
* This is were we decorate the dropdownToggle directive
* in order to change the way the document click handler works
*/
myApp.config(function($provide){
'use strict';
$provide.decorator('dropdownToggleDirective', [
'$delegate',
'$document',
function ($delegate, $document) {
var directive = $delegate[0];
var openElement = null;
var closeMenu = angular.noop;
function handler(e){
var elm = angular.element(e.target);
if(!elm.parents('.dropdown-menu').length){
e.stopPropagation();
e.preventDefault();
}
closeMenu();
// After closing the menu, we remove the all-seeing handler
// to allow the application click events to work nnormally
$document[0].removeEventListener('click', handler, true);
}
directive.compile = function(){
return function(scope, element) {
scope.$watch('$location.path', closeMenu);
element.parent().bind('click', closeMenu);
element.bind('click', function (event) {
var elementWasOpen = (element === openElement);
event.preventDefault();
event.stopPropagation();
if (!!openElement) {
closeMenu();
}
if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
element.parent().addClass('open');
openElement = element;
closeMenu = function (event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
$document.unbind('click', closeMenu);
element.parent().removeClass('open');
closeMenu = angular.noop;
openElement = null;
};
// We attach the click handler by specifying the third "useCapture" parameter as true
$document[0].addEventListener('click', handler, true);
}
});
};
};
return $delegate;
}
]);
});
UPDATE:
Note that the updated custom handler will prevent bubbling unless the target element is the actual drop-down option. This will solve the problem where click event was being prevented even when clicking on the drop-down options.
This still won't prevent the event to bubble down to row (from drop-down option), but this is something that's not in any way related to the drop-down directive. Anyway, to prevent such bubbling you can pass the $event
object to ng-click
expression function and use that object to stop the even to bubble down to the table row:
<div ng-controller="DropdownCtrl">
<table>
<tr ng-click="clicked('row')">
<td>
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle">
Action <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="choice in items">
<a ng-click="clicked('link element', $event)">{{choice}}</a>
</li>
</ul>
</div>
</td>
</tr>
</table>
</div>
function DropdownCtrl($scope) {
$scope.items = [
"Action",
"Another action",
"Something else here"
];
$scope.clicked = function(what, event) {
alert(what + ' clicked');
if(event){
event.stopPropagation();
event.preventDefault();
}
}
}
ng-click="$event.stopPropagation()"
on eacha
but it won't solve the click-anywhere-to-collapse-the-dropdown-menu scenario where ang-click
call is fired after the jQueryclick
event bound to$document
is completed. – BayoucloseMenu
,event.preventDefault()
andevent.stopPropagation()
won't cancel the underlyingng-click
. I assume that this is due to jQuery and Angular not sharing the same call stack. – Bayou$document
is a jQuery wrapper for window.document and that the$event
variable is not accessible in the context ofcloseMenu
. Does that make any sense? – Bayou