Click everywhere but here event

Was wondering how I would implement a 'Click everywhere but on this element' event.

I have something that you can compare to a list of files in a file explorer. You can select certain elements but if you click outside the element controller it needs to deselect everything.

enter image description here

Added a screenshot to make it more clear. So what I want to do is that if I click anywhere but on the language elements it should fire an event.

Update

To clarify I am not asking how I can do this with jQuery.

Answers:

Answer

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>
Answer

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.

Answer

Found the anwser on this blog post.

Directive:

app.directive('documentClick', function ($document, $parse) {

  var linkFunction = function ($scope, $element, $attributes) {

    var scopeExpression = $attributes.documentClick;
    var invoker = $parse(scopeExpression);

    $document.on('click', function (event) {

      $scope.$apply(function () {
        invoker($scope, { $event: event });
      });

    });

  };

  return linkFunction;

});

Controller:

app.controller('PageCtrl', function ($scope) {

  $scope.click = function (e) {
    if (!$(e.target).is('.language')) {
      //do stuff
    }
  };

});

View:

<body ng-controller='PageCtrl' document-click='click($event)'></body>
Answer

Here is a variant based on Max's solution but more natural in terms of standard angular events directives:

app.directive('clickOut', function($document) {
    return {
        restrict: 'A',
        scope: {
            clickOut: '&'
        },
        link: function (scope, element) {
            var handler = function(event) {
                if (!element[0].contains(event.target)) {
                    scope.$apply(function () {
                        scope.clickOut({ $event : event }); 
                    });
                }
            };

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

Usage

<div click-out="myFunction()"></div>

Passing click event

<div click-out="myFunction($event)"></div>
Answer

Benefit of this solution:

  1. You only need one bind to $(document). Other firing of events would depend on the $emit of scope events.
  2. You could use both expressions click-elsewhere="show=false", and click-elsewhere="fn()", thanks to $parse.

Code:

// broadcast click event within AppCtrl
app.controller('AppCtrl', function($rootScope) {
  $(document).on('click', function(e) {
    // use $emit so the event stays inside $rootScope
    $rootScope.$emit('click', {target: e.target});
  };
};

app.directive('clickElsewhere', function($rootScope) {
  return {
    restrict: 'A',
    compile: function($element, attr) {
      // store fn in compile so it only execute once
      var fn = $parse(attr['clickElsewhere']); 

      return function(scope, element) {
        var offEvent = $rootScope.$on('click', function(event, target) {
          if ( (element.find($(target)).length) || element.is($(target)) ) return;

          scope.$apply(function() {
            fn(scope, {$event: event});
          });
        });

        scope.$on('$destroy', offEvent);
      };
    }
  };

Usage in HTML:

  1. click-elsewhere="fn()"
  2. click-elsewhere="show=false"
Answer

it's possibly a bit out of context, but you could classify ( with for instance "selected" ) all selected items ans when a user clicks on the 'do not click here' element, you can declassify all items currently classified "selected", and classify the specific element...

Answer

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()

Answer

The simplest way is to check the element's scope.$id and the clickScope.$id (the click event target scope.$id).

link: function(scope, element, attrs) { 
    //close element when mouse click outside
    var documentClickHandler = function(event) {
        var clickScope = angular.element(event.target).scope();
        if (clickScope.$id != scope.$id) {
            //clickScope.$parent is for clicking on the directive children scope
            if(clickScope.$parent === null || 
               clickScope.$parent.$id != scope.$id){
               //Click everywhere but on this element
               //Do anything you want here, like close your element;
            }
        }

    };

    $document.on('click', documentClickHandler);

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

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.