testing
testing

Reputation: 2343

How to detect element click efficiently in Angular?

Here is my link function for my directive

    function linkFunc(scope, element, attr){

        // Detect Element Click Logic
        scope.myCtrl.clickedElsewhere = {value: true};

        $document.on('click', function(){
            scope.myCtrl.clickedElsewhere.value = true;
            scope.$apply();
        });

        element.on('click', function(){
            event.stopPropagation();
            scope.myCtrl.clickedElsewhere.value = false;
            scope.$apply();
        });
        // End Detect Element Click Logic

    }

As we can see, we used $document.on() and scope.apply , this means that for every click on anywhere in the document we will trigger a digest cycle. If we have alot of $watch triggers this might cause the web page to slow down. Although this implementation is not every efficient, I can't think of other ways to detect on element click and off element click for expanding and contracting my element.

Can someone provide some insight?

Thanks

Upvotes: 3

Views: 2524

Answers (3)

Justin Ober
Justin Ober

Reputation: 849

You could just create an click off directive

myApp.directive('clickOff', function($parse, $document) {
  var dir = {
    compile: function($element, attr) {
      // Parse the expression to be executed
      // whenever someone clicks _off_ this element.
      var fn = $parse(attr["clickOff"]);
      return function(scope, element, attr) {
        // add a click handler to the element that
        // stops the event propagation.
        element.bind("click", function(event) {
          event.stopPropagation();
        });
        angular.element($document[0].body).bind("click",function(event) {
            scope.$apply(function() {
                fn(scope, {$event:event});
            });
        });
      };
    }
  };
  return dir;
});

Usage:

<div ng-app="myApp">
  <button ng-click="show=true;" click-off="show=false;">
      Click Me
  </button>
  <div class="message" ng-show="show" ng-init="show = false">
    You clicked on the button. Now click anywhere else...
  </div>
</div>

Example: https://jsfiddle.net/oytdwyxj/

Upvotes: 2

charlietfl
charlietfl

Reputation: 171669

it would be a lot more efficient to check scope.myCtrl.clickedElsewhere.value before calling $apply() in the document click handler:

$document.on('click', function(){
   if(!scope.myCtrl.clickedElsewhere.value){
       scope.myCtrl.clickedElsewhere.value = true;
       scope.$apply();
   }
});

This will prevent needless digests when there is no change in it's status.


You could also remove this click listener and reapply it when you click on element:

function docHandler() {
  if (!scope.myCtrl.clickedElsewhere.value) {
    scope.myCtrl.clickedElsewhere.value = true;
    scope.$apply();
    $document.off('click');//remove event listener completely
  }
}

element.on('click', function() {
  event.stopPropagation();
  scope.myCtrl.clickedElsewhere.value = false;
  scope.$apply();
  $document.on('click', docHandler);// add document listener
});

Upvotes: 3

Rodrigo Branas
Rodrigo Branas

Reputation: 538

You could create two directives, with a parent/child relationship between them.

Take a look at the accordion directive that I created, which I believe that is what you need to in order to figure out your situation.

Directives:

app.directive("accordion", function () {
    return {
        template: "<div ng-transclude></div>",
        restrict: "E",
        scope: {
            closeall: "@"
        },
        transclude: true,
        replace: true,
        controller: function ($scope, $element, $attrs) {
            var itensScope = [];

            var addItemScope = function (scope) {
                itensScope.push(scope);
            };

            var closeItens = function () {
                if ($scope.closeall == "false") return;
                angular.forEach(itensScope, function (scope) {
                    scope.showItem = false;
                });
            }
            return {
                addItemScope: addItemScope,
                closeItens: closeItens
            };
        }
    };
});
app.directive("accordionItem", function () {
    return {
        template: "<div><div class='item' ng-class='{itemClose: !showItem}'>{{title}}</div><div class='itemInformation' ng-show='showItem' ng-transclude></div></div>",
        restrict: "E",
        transclude: true,
        replace: true,
        scope: {
            title: "@"
        },
        require: "^accordion",
        link: function (scope, element, attrs, ctrl, transcludeFn) {
            ctrl.addItemScope(scope);
            element.bind("click", function () {
                ctrl.closeItens();
                scope.$apply(function () { 
                    scope.showItem = !scope.showItem; 
                });
            });
        }
    };
});

Usage:

<accordion closeall="true">
        <accordion-item title="A">
            <p>
                A
            </p>
        </accordion-item>
        <accordion-item title="B">
            <p>
                B
            </p>
        </accordion-item>
</accordion>

I created this example a while ago and it is available in my GitHub: https://github.com/rodrigobranas/branas-angular-ui

Upvotes: 0

Related Questions