188

Here's an example. Let's say I want to have an image overlay like a lot of sites. So when you click a thumbnail, a black overlay appears over your whole window, and a larger version of the image is centered in it. Clicking the black overlay dismisses it; clicking the image will call a function that shows the next image.

The html:

<div ng-controller="OverlayCtrl" class="overlay" ng-click="hideOverlay()">
    <img src="http://some_src" ng-click="nextImage()"/>
</div>

The javascript:

function OverlayCtrl($scope) {
    $scope.hideOverlay = function() {
        // Some code to hdie the overlay
    }
    $scope.nextImage = function() {
        // Some code to find and display the next image
    }
}

The problem is that with this setup, if you click the image, both nextImage() and hideOverlay() are called. But what I want is for only nextImage() to be called.

I know you can capture and cancel the event in the nextImage() function like this:

if (window.event) {
    window.event.stopPropagation();
}

...But I want to know if there's a better AngularJS way of doing it that won't require me to prefix all of the functions on elements inside the overlay with this snippet.

cincodenada
  • 2,877
  • 25
  • 35
cilphex
  • 6,006
  • 6
  • 34
  • 44

10 Answers10

200

What @JosephSilber said, or pass the $event object into ng-click callback and stop the propagation inside of it:

<div ng-controller="OverlayCtrl" class="overlay" ng-click="hideOverlay()">
  <img src="http://some_src" ng-click="nextImage($event)"/>
</div>
$scope.nextImage = function($event) {
  $event.stopPropagation();
  // Some code to find and display the next image
}
Stewie
  • 60,366
  • 20
  • 146
  • 113
174

Use $event.stopPropagation():

<div ng-controller="OverlayCtrl" class="overlay" ng-click="hideOverlay()">
    <img src="http://some_src" ng-click="nextImage(); $event.stopPropagation()" />
</div>

Here's a demo: http://plnkr.co/edit/3Pp3NFbGxy30srl8OBmQ?p=preview

Joseph Silber
  • 214,931
  • 59
  • 362
  • 292
  • I get `ReferenceError: $event is not defined` in the console. – cilphex Mar 04 '13 at 02:32
  • Yes, it does... it also works when I write a separate simple version from scratch. I'm trying to figure out what could make it not work in my development code. Dunno :\ – cilphex Mar 04 '13 at 02:41
  • @cilphex - Are you using an up-to-date version of Angular? – Joseph Silber Mar 04 '13 at 02:41
  • Yes - just found the problem. When testing I tried putting `$event.stopPropagation()` in a place where it doesn't work. Forgot to remove it. So even when I put it where it did work, it was being called a second time where it didn't. Got rid of the dysfunctional duplicate and it's successful. Thanks for your help. – cilphex Mar 04 '13 at 02:48
  • This also works with click. Thank you! – Torben Nov 12 '21 at 15:55
101

I like the idea of using a directive for this:

.directive('stopEvent', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            element.bind('click', function (e) {
                e.stopPropagation();
            });
        }
    };
 });

Then use the directive like:

<div ng-controller="OverlayCtrl" class="overlay" ng-click="hideOverlay()">
    <img src="http://some_src" ng-click="nextImage()" stop-event/>
</div>

If you wanted, you could make this solution more generic like this answer to a different question: https://stackoverflow.com/a/14547223/347216

Community
  • 1
  • 1
David Ipsen
  • 1,513
  • 1
  • 15
  • 22
  • 2
    If your element is added/removed from the DOM dynamically, don't forget to watch your element for a '$destroy' event so you can unbind the handler. E.g., element.on('$destroy', function() { /* do the unbinding here */ }); – broc.seib Sep 03 '14 at 14:59
  • is `stop-event` an html attribute? I couldn't find it on Mozilla Developer Network or anywhere else. – Ehtesh Choudhury Nov 12 '14 at 19:21
  • 3
    @EhteshChoudhury - "stop-event" is the new directive I created in the JS snippet above. Check out more about declaring and using directives here: https://docs.angularjs.org/guide/directive – David Ipsen Nov 12 '14 at 20:37
  • Nice solution! I actually registered the [`angular-eat-event`](https://github.com/gion/angular-eat-event) directive on bower, which works in a similar way! – gion_13 Sep 21 '15 at 20:26
  • it doesn't seem to work, ng-click evaluates before the directive. so I can prevent directive from executing but not the other way around. – Rafael Jan 26 '16 at 07:29
32

Sometimes, it may make most sense just to do this:

<widget ng-click="myClickHandler(); $event.stopPropagation()"/>

I chose to do it this way because I didn't want myClickHandler() to stop the event propagation in the many other places it was used.

Sure, I could've added a boolean parameter to the handler function, but stopPropagation() is much more meaningful than just true.

Michael Scheper
  • 6,514
  • 7
  • 63
  • 76
  • 2
    I always liked this version, seems the nesting of element shouldn't related to the function that i am calling – mcfedr Sep 26 '15 at 16:23
  • I had to add `$event.stopPropagation()` to internal elements. – Kishor Pawar Feb 10 '17 at 13:15
  • @KishorPawar: Why? Did the way I did it in the answer not work for you? If it didn't, it'd be good for us to find out whether it's because of a change in the way Angular works, or a problem in your code. – Michael Scheper Feb 21 '17 at 17:43
  • @MichaelScheper I am having `checkbox` wrapped in `div` element. My `div` is taking 12 columns and `checkbox` is taking say 3 columns. I wanted to call a function `A` on click of `div` and function `B` on click of `checkbox`. so when I was clicking `checkbox` both functions were getting invoked. When I add `ng-click="$event.stopPropagation()"` to `checkbox`, I got only function `B` invoked. – Kishor Pawar Feb 21 '17 at 18:03
  • @KishorPawar: Ah. Yes, I would expect this, and indeed, the point of `stopPropagation()` is to stop the event propagating to parent elements. I'm not sure how you're calling `B` since it's not specififed in `ng-click`, but the point of the answer is that you can do this: ``. You don't have to include the `stopPropagation()` function in `B`. – Michael Scheper Feb 23 '17 at 17:03
9

This works for me:

<a href="" ng-click="doSomething($event)">Action</a>

this.doSomething = function($event) {
  $event.stopPropagation();
  $event.preventDefault();
};
Andrey Shatilov
  • 576
  • 6
  • 10
  • I wanted to handle mouse clicks with or without pressing the Ctrl key. To stop selection (and highlight) of HTML elements in the browser (IE 11 in my case) when the user clicks various items with holding down the Ctrl key, I had to use the ng-mousedown event and cancelling the default behavior via $event.preventDefault() – pholpar Sep 20 '16 at 06:57
4

If you don't want to have to add the stop propagation to all links this works as well. A bit more scalable.

$scope.hideOverlay( $event ){

   // only hide the overlay if we click on the actual div
   if( $event.target.className.indexOf('overlay') ) 
      // hide overlay logic

}
Hampus Ahlgren
  • 519
  • 1
  • 5
  • 15
  • 2
    This solution works for the provided example (which is great!), however it doesn't work when the outer div has n or more nested elements prior to the href / ng-click. Then when the hideOverlay() callback is invoked the target is one of the nested elements and not the outermost element with an ng-click directive. To handle nested elements conceivably one could use jquery to get the parents and see if the first clickable (a, ng-click, etc.) parent had the overlay class, but that looks a bit cumbersome... – Amir Feb 24 '14 at 21:45
  • console.log("potatooverlay".indexOf("overlay") != -1); console.log(/\boverlay\b/g.test("potatooverlay")); –  Jun 30 '14 at 18:21
  • angular will let you do `event.target.classList.indexOf('overlay')` And that trick works fine even when the div is nested in other divs – deltree Jan 21 '16 at 04:56
0

If you insert ng-click="$event.stopPropagation" on the parent element of your template, the stopPropogation will be caught as it bubbles up the tree, so you only have to write it once for your entire template.

tbranch
  • 41
  • 3
0

You can register another directive on top of ng-click which amends the default behaviour of ng-click and stops the event propagation. This way you wouldn't have to add $event.stopPropagation by hand.

app.directive('ngClick', function() {
    return {
        restrict: 'A',
        compile: function($element, attr) {
            return function(scope, element, attr) {
                element.on('click', function(event) {
                    event.stopPropagation();
                });
            };
        }
    }
});
bert
  • 1,556
  • 13
  • 15
0
<div ng-click="methodName(event)"></div>

IN controller use

$scope.methodName = function(event) { 
    event.stopPropagation();
}
Paul Rooney
  • 20,879
  • 9
  • 40
  • 61
Dinesh Jain
  • 149
  • 11
0

In my case event.stopPropagation(); was making my page refresh each time I pressed on a link so I had to find another solution.

So what I did was to catch the event on the parent and block the trigger if it was actually coming from his child using event.target.

Here is the solution:

if (!angular.element($event.target).hasClass('some-unique-class-from-your-child')) ...

So basically your ng-click from your parent component works only if you clicked on the parent. If you clicked on the child it won't pass this condition and it won't continue it's flow.

paulalexandru
  • 9,218
  • 7
  • 66
  • 94