451

I have a click Event on a table row and in this row there is also a delete Button with a click Event. When i click the delete button the click Event on the row is also fired.

Here is my code.

<tbody>
  <tr ng-repeat="user in users" class="repeat-animation" ng-click="showUser(user, $index)">
    <td>{{user.firstname}}</td>
    <td>{{user.lastname}}</td>
    <td>{{user.email}}</td>
    <td><button class="btn red btn-sm" ng-click="deleteUser(user.id, $index)">Delete</button></td>
  </tr>
</tbody>

How can I prevent that the showUser Event is fired when i click the delete Button in the table cell?

Mosh Feu
  • 28,354
  • 16
  • 88
  • 135
michael_knight
  • 4,921
  • 3
  • 17
  • 10

5 Answers5

840

ngClick directive (as well as all other event directives) creates $event variable which is available on same scope. This variable is a reference to JS event object and can be used to call stopPropagation():

<table>
  <tr ng-repeat="user in users" ng-click="showUser(user)">
    <td>{{user.firstname}}</td>
    <td>{{user.lastname}}</td>
    <td>
      <button class="btn" ng-click="deleteUser(user.id, $index); $event.stopPropagation();">
        Delete
      </button>
    </td>              
  </tr>
</table>

PLUNKER

Brendan Moore
  • 1,131
  • 7
  • 13
Stewie
  • 60,366
  • 20
  • 146
  • 113
  • 2
    I could not figure out if this is available in the controller code - $scope.$event did not seem to work. Any ideas? – user1338062 Feb 21 '14 at 10:57
  • 93
    @event object is created inside the ng-click directive, and it is available to you to pass it on to your `ng-click` handler function: `ng-click="deleteUser(user.id, $event)"`. – Stewie Feb 21 '14 at 12:10
  • 6
    Thanks, kind of figured that one, but I think it still stinks :) – user1338062 Feb 21 '14 at 14:02
  • 2
    I think that it's an antipattern to execute multiple expressions like this. Why not just pass $event as a third argument to deleteUser() and then stopPropagation() inside that function? – djheru May 08 '15 at 13:45
  • 2
    @djheru The multiple expressions do seem clunky, but putting this sort of event code in the controller seems weird. We could solve both problems by putting `ng-click="$event.stopPropagation()"` on the _parent_ element (the `td` in this case). – Bennett McElwee Aug 06 '15 at 04:57
  • 20
    I had to add `$event.preventDefault()` too, otherwise calling `$event.stopPropagation()` was redirecting me to the root of my app when clicking the button. – Desty Oct 21 '15 at 16:13
  • $event.stopPropagation() is the best to stop event bubbling – Dinesh Jain Jun 07 '17 at 02:44
  • 1
    @DineshJain well, that and `event.stopImmediatePropagation()` are the *only* way to stop event bubbling. – pilau Jul 25 '17 at 12:09
  • It seems only $event.preventDefault() is needed in this method – Binod Kalathil Dec 05 '18 at 09:32
136

An addition to Stewie's answer. In case when your callback decides whether the propagation should be stopped or not, I found it useful to pass the $event object to the callback:

<div ng-click="parentHandler($event)">
  <div ng-click="childHandler($event)">
  </div>
</div>

And then in the callback itself, you can decide whether the propagation of the event should be stopped:

$scope.childHandler = function ($event) {
  if (wanna_stop_it()) {
    $event.stopPropagation();
  }
  ...
};
Timo Tijhof
  • 10,032
  • 6
  • 34
  • 48
korya
  • 2,026
  • 1
  • 15
  • 12
11

I wrote a directive which lets you limit the areas where a click has effect. It could be used for certain scenarios like this one, so instead of having to deal with the click on a case by case basis you can just say "clicks won't come out of this element".

You would use it like this:

<table>
  <tr ng-repeat="user in users" ng-click="showUser(user)">
    <td>{{user.firstname}}</td>
    <td>{{user.lastname}}</td>
    <td isolate-click>
      <button class="btn" ng-click="deleteUser(user.id, $index);">
        Delete
      </button>
    </td>              
  </tr>
</table>

Keep in mind that this would prevent all clicks on the last cell, not just the button. If that's not what you want you may want to wrap the button like this:

<span isolate-click>
    <button class="btn" ng-click="deleteUser(user.id, $index);">
        Delete
    </button>
</span>

Here is the directive's code:

angular.module('awesome', []).directive('isolateClick', function() {
    return {
        link: function(scope, elem) {
            elem.on('click', function(e){
                e.stopPropagation();
            });
        }
   };
});
Jens
  • 5,767
  • 5
  • 54
  • 69
  • Nice! I've renamed your directive to `ngClick`, as I actually never want to propagate an already handled event. – maaartinus May 06 '16 at 00:54
  • 1
    Be careful with that. Some other plugins might actually want to listen those clicks. If you use tooltips that close when clicking outside they may never close, because the outside clicks are not being propagated to the body. – Jens May 06 '16 at 18:16
  • That's a good point, but this problem would happen with your directive too. Maybe I should just add a property like `ignoreNgClick=true` to the event and handle it... somehow. Ideally, in the original `ngClick` directive, but modifying it sounds dirty. – maaartinus May 06 '16 at 18:22
  • Yes it does, that's why I wouldn't use this directive unless I really need it. Once I even had to make another directive to continue the click propagation due to this very reason. So I had a an isolate-click directive, then a couple parents up I had another continue-click directive. This way the click was only skipped for the middle elements. – Jens May 06 '16 at 19:39
1

In case that you're using a directive like me this is how it works when you need the two data way binding for example after updating an attribute in any model or collection:

angular.module('yourApp').directive('setSurveyInEditionMode', setSurveyInEditionMode)

function setSurveyInEditionMode() {
  return {
    restrict: 'A',
    link: function(scope, element, $attributes) {
      element.on('click', function(event){
        event.stopPropagation();
        // In order to work with stopPropagation and two data way binding
        // if you don't use scope.$apply in my case the model is not updated in the view when I click on the element that has my directive
        scope.$apply(function () {
          scope.mySurvey.inEditionMode = true;
          console.log('inside the directive')
        });
      });
    }
  }
}

Now, you can easily use it in any button, link, div, etc. like so:

<button set-survey-in-edition-mode >Edit survey</button>
Heriberto Magaña
  • 882
  • 10
  • 11
-14
<ul class="col col-double clearfix">
 <li class="col__item" ng-repeat="location in searchLocations">
   <label>
    <input type="checkbox" ng-click="onLocationSelectionClicked($event)" checklist-model="selectedAuctions.locations" checklist-value="location.code" checklist-change="auctionSelectionChanged()" id="{{location.code}}"> {{location.displayName}}
   </label>



$scope.onLocationSelectionClicked = function($event) {
      if($scope.limitSelectionCountTo &&         $scope.selectedAuctions.locations.length == $scope.limitSelectionCountTo) {
         $event.currentTarget.checked=false;
      }
   };

Hems
  • 1
  • 7
    Would you add some explanation of why you think this answers the question? – paisanco Jul 16 '15 at 01:11
  • It kind of answers the question. It shows how to pass in the event to the callback which is a more than what the initial question figured out. – hewstone Nov 22 '16 at 02:49