AngularJS: Stopping event propagation on ng-click

Let’s assume you have a link containing an additional icon e.g.:

<a href="" ng-click="doSomething()">
	<span class="glyphicon glyphicon-th-list" ng-click="doSomethingElse()"></span>
</a>

(this whole article of course only make sense if your <a> tag contains more than this or is displayed in such a way that it’s larger than the icon)

When you click outside of the <span> doSomething() will be called. But when you click on the <span> both doSomethingElse() and doSomething() will be called. This is because of event propagation. The click is processed by doSomethingElse and then the event propagates to the parent of the <span> tag i.e. the <a> tag and triggers doSomething().

Actually the fact that it’s an <a> tag doesn’t make much of a difference. If you replace the <a> tag by a <div> tag, you’ll get the same results.

In plain old JavaScript, you’d return false in your click handler to prevent event propagation. Doing this in doSomethingElse() unfortunately doesn’t help. doSomething() will still be called.

The code defining the ngClick directive looks like this (in AngularJS 1.3.15):

forEach(
  'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
  function(eventName) {
    var directiveName = directiveNormalize('ng-' + eventName);
    ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
      return {
        restrict: 'A',
        compile: function($element, attr) {
          // We expose the powerful $event object on the scope that provides access to the Window,
          // etc. that isn't protected by the fast paths in $parse.  We explicitly request better
          // checks at the cost of speed since event handler expressions are not executed as
          // frequently as regular change detection.
          var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
          return function ngEventHandler(scope, element) {
            element.on(eventName, function(event) {
              var callback = function() {
                fn(scope, {$event:event});
              };
              if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
                scope.$evalAsync(callback);
              } else {
                scope.$apply(callback);
              }
            });
          };
        }
      };
    }];
  }
);

So it’s not returning the return value of the function used in ng-click. So we have two ways of solving this.

The first solution is to pass the $event parameter to your function and call both stopPropagation() and preventDefault():

<a href="" ng-click="doSomething()">
	<span class="glyphicon glyphicon-th-list" ng-click="doSomethingElse($event)"></span>
</a>

And:

this.doSomethingElse = function($event) {
	// do something else
	$event.stopPropagation();
	$event.preventDefault();
};

If you do not want to add this code in all ngClick handlers, you can also use a directive which will install a click handler on the element and return false:

<a href="" ng-click="doSomething()">
	<span class="glyphicon glyphicon-th-list" ng-click="doSomethingElse()" stop-propagation></span>
</a>

And:

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

Both ways work. I ended up using the first one because it doesn’t involve going around AngularJS to get this solved but it does pollute your JavaScript code. So use the second one if it’s a problem for you and you’d rather only add an attribute to your HTML code. The advantage of the second approach is that you can easily add this behavior to any element by just adding the attribute.

One thought on “AngularJS: Stopping event propagation on ng-click

Leave a Reply

Your email address will not be published. Required fields are marked *