Angular ng-click event delegation
Asked Answered
M

4

28

So if i have a ul with 100 li's should there be ng-clicks in each li or is there a way to bind the event to the ul and delegate it to the li's kind of what jquery does? Would this be better or worse? Are we having 100 events or is it just one event in the end?

Martguerita answered 20/12, 2012 at 5:3 Comment(1)
a friend of mine pointed this out: plnkr.co/edit/uC49BsKmvvDvQNJ72r0v?p=preview it is possible to achiev this with plane angularPaschall
S
21

It seems angular doesn't do event delegation with repeaters. Someone opened an issue on github about it. The argument is if it actually leads to better performance.

There might be a workaround but it would require jQuery. It consists of creating a special directive to be used on a parent element and register the listener on its dom node.

Here's an example, that is passed a function name to be called when a children node is clicked, and is also passed a selector to help identify which children nodes to listen to. Since angular's jquery implementation only gives us the bind method - which is limited to registering event listeners to the actual element - we need to load jQuery to have access to either the on or delegate method.

HTML

<ul click-children='fun' selector='li'>
    <li ng-repeat="s in ss" data-index="{{$index}}">{{s}}</li>
</ul>

The function defined is defined in the controller and it expects to be passed an index

$scope.fun = function(index){
    console.log('hi from controller', index, $scope.ss[index]);      
};

The directive uses $parse to convert an expression into a function that will be called from the event listener.

app.directive('clickChildren', function($parse){
  return {
    restrict: 'A',
    link: function(scope, element, attrs){       
      var selector = attrs.selector;
      var fun = $parse(attrs.clickChildren);   
      element.on('click', selector, function(e){       
        // no need to create a jQuery object to get the attribute 
        var idx = e.target.getAttribute('data-index');        
        fun(scope)(idx);        
      });
    }
  };
});

Plunker: http://plnkr.co/edit/yWCLvRSLCeshuw4j58JV?p=preview


Note: Functions can be delegated to directives using isolate scopes {fun: '&'}, which is worth a look, but this increases complexity.

Strander answered 20/12, 2012 at 7:14 Comment(5)
Hey thank you very much jm- for your answer. Ive always been curious as well if this would be more perfoment or not. Going to run some js perfs to see id i can catch the difference in time. This solution is quite cool!Martguerita
You'll also want to listen for the $destroy event, to remove the event listener from element. Otherwise I believe there's a memory leak when the element gets removed from the DOM.Wellheeled
Yo need to use e.currentTarget instead of e.target. e.target is element under click. e.currentTarget is element to which the event attached (even delegated event). For example if html is <a><span>text</span></a> and delegation selector is a then e.target will be span element and e.currentTarget will be a element.Joettajoette
@Martguerita have you ran those perfs tests? What can you say about it? Is it worth or not to do event delegation in angular?Verst
@Verst I haven't unfortunately. Given todays world with angular 2 I'm not even sure its worth exploringMartguerita
R
6

Working off of jm-'s example here, I wrote a more concise and flexible version of this directive. Thought I'd share. Credit goes to jm- ;)

My version attempts to call the function name as $scope[ fn ]( e, data ), or fails gracefully.

It passes an optional json object from the element which was clicked. This allows you to use Angular expressions and pass numerous properties to the method being called.

HTML

<ul delegate-clicks="handleMenu" delegate-selector="a">
  <li ng-repeat="link in links">
    <a href="#" data-ng-json='{ "linkId": {{link.id}} }'>{{link.title}}</a>
  </li>
</ul>

Javascript

Controller Method

$scope.handleMenu = function($event, data) {
  $event.preventDefault();
  $scope.activeLinkId = data.linkId;
  console.log('handleMenu', data, $scope);
}

Directive Constructor

// The delegateClicks directive delegates click events to the selector provided in the delegate-selector attribute.
// It will try to call the function provided in the delegate-clicks attribute.
// Optionally, the target element can assign a data-ng-json attribute which represents a json object to pass into the function being called. 
// Example json attribute: <li data-ng-json='{"key":"{{scopeValue}}" }'></li>
// Use case: Delegate click events within ng-repeater directives.

app.directive('delegateClicks', function(){
  return function($scope, element, attrs) {
    var fn = attrs.delegateClicks;
    element.on('click', attrs.delegateSelector, function(e){
      var data = angular.fromJson( angular.element( e.target ).data('ngJson') || undefined );
      if( typeof $scope[ fn ] == "function" ) $scope[ fn ]( e, data );
    });
  };
});

I'd love to hear feedback if anyone wishes to contribute.

I didn't test the handleMenu method since I extracted this from a more complex application.

Rosalie answered 29/1, 2013 at 23:33 Comment(1)
Nice work. See my enhancement using an adaptation of georg's answer to "Parse object dot notation to retrieve a value of an object".Kellerman
K
0

Starting from BradGreens' delegateClicks above, I've adapted some code from georg which allows me to place the handleMenu function deeper in $scope (e.g. $scope.tomato.handleMenu).

app.directive('delegateClicks', function () {
    return function ($scope, element, attrs) {
        var fn = attrs.delegateClicks.split('.').reduce(function ($scope, p) { return $scope[p] }, $scope); 
        element.on('click', attrs.delegateSelector, function (e) {
            var data = angular.fromJson(angular.element(e.target).data('ngJson') || undefined);
            if (typeof fn == "function") fn(e, data);
        });
    };
});
Kellerman answered 26/9, 2015 at 13:53 Comment(0)
A
0

According to the issue on GitHub there is no performance advantage to delegating event handling.

Simply use the ng-click directive with the item, $index, and $event:

<ul>
   <li ng-repeat="item in collection"
       ng-click="lineClicked(item, $index, $event)">
      {{item}}
   </li>
</ul>
$scope.lineClicked = function(item, index, event) {
    console.log("line clicked", item, index);
    console.log(event.target)
});

For more information, see

Augend answered 15/12, 2019 at 23:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.