Click everywhere but here event

EDIT: There were a couple of problems in this old, old answer.

*Also: Marking Community Wiki (no points for me) because errors

  1. N calls for N uses of the directive. This probably isn't desirable for uses within the same scope with matching expressions.

  2. NOTHING WAS TEARING DOWN THE EVENT HANDLERS!!!! BAD! BAD! BAD!

So, I'm updating this answer. Hopefully it didn't cause anyone too much trouble.

Updated answer

Here's a new plunker with those issues fixed ... there are likely other things that individual application developers will run into. This is just an example of how to handle this problem.

app.factory('clickAnywhereButHereService', function($document){
  var tracker = [];
  
  return function($scope, expr) {
    var i, t, len;
    for(i = 0, len = tracker.length; i < len; i++) {
      t = tracker[i];
      if(t.expr === expr && t.scope === $scope) {
        return t;    
      }
    }
    var handler = function() {
      $scope.$apply(expr);
    };
    
    $document.on('click', handler);
    
    // IMPORTANT! Tear down this event handler when the scope is destroyed.
    $scope.$on('$destroy', function(){
      $document.off('click', handler);
    });
    
    t = { scope: $scope, expr: expr };
    tracker.push(t);
    return t;
  };
});

app.directive('clickAnywhereButHere', function($document, clickAnywhereButHereService){
  return {
    restrict: 'A',
    link: function(scope, elem, attr, ctrl) {
      var handler = function(e) {
        e.stopPropagation();
      };
      elem.on('click', handler);
      
      scope.$on('$destroy', function(){
        elem.off('click', handler);
      });
      
      clickAnywhereButHereService(scope, attr.clickAnywhereButHere);
    }
  };
});

Original answer (with fixes for teardown of event handlers)

You were close with the one answer you've found, but I've put together a plunk for you to show you what it was missing.

app.directive('clickAnywhereButHere', function($document){
  return {
    restrict: 'A',
    link: function(scope, elem, attr, ctrl) {
      var elemClickHandler = function(e) {
        e.stopPropagation();
      };
      
      var docClickHandler = function() {
        scope.$apply(attr.clickAnywhereButHere);
      };
      
      elem.on('click', elemClickHandler);
      $document.on('click', docClickHandler);
      
      // teardown the event handlers when the scope is destroyed.
      scope.$on('$destroy', function() {
        elem.off('click', elemClickHandler);
        $document.off('click', docClickHandler);
      });
    }
  }
})

HTML

<a click-anywhere-but-here="clickedSomewhereElse()" 
  ng-click="clickedHere()">Don't Click Me!</a>

The problem with the current accepted answer is that if you use the directive multiple times, every DOM element which has the directive attached will prevent bubbling (so if you have it on two elements, and you click in either, the callbacks of both will be blocked).

EDIT - avoid jQuery, clean up -- Define a function on your scope, and pass it to this directive directly (without parentheses), and the event will be passed to it when called.

app.directive('clickAnywhereButHere', function($document, $parse) {
    return {
        restrict: 'A',
        scope: {
            callback : '=clickAnywhereButHere'
        },
        link: function(scope, element, attr, ctrl) {
            var handler = function(event) {
                if (!element[0].contains(event.target)) {
                    scope.callback(event);
                 }
            };

            $document.on('click', handler);
            scope.$on('$destroy', function() {
                $document.off('click', handler);
            });
        }
    }
});

Usage in HTML

<a click-anywhere-but-here="myFunction"></a>

Usage in Controller

 $scope.myFunction = function (event) { ... } 

-

Note that you may need to wrap scope.callback(event) with scope.$apply()


If you have alot of elements that needs this directive, here is another solution that is performance optimized. (Example a list with 100+ rows, each with this directive)

This will always contain only one $document listener

angular.module('app').directive('clickElsewhere', ['$document', function ($document) {
return {
  link: function postLink(scope, element, attr) {

    var elsewhere = true;

    element.on('click', function(e) {
      elsewhere = false;
      $document.off('click', clickElsewhere);
      $document.on('click', clickElsewhere);
    });

    var clickElsewhere = function() {
      if (elsewhere) {
        scope.$apply(attr.clickElsewhere);
        $document.off('click', clickElsewhere);
      }
      elsewhere = true;
    };

  }
};
}]);

Problem with solution from Max Bates is that all directives is adding a listener for the $document.on('click', function(...)); event which makes performance issues.

Problem with accepted answer has Max Bates allready stated.