5
\$\begingroup\$

In use cases where web applications utilize ng-repeat heavily and also rely on event triggers for each item in the collection, such as ng-click and the like, watchers become too numerous and may adversely affect performance when the number of items grow large.

To remedy this problem, an event delegation directive could handle the work. It is unfortunate that AngularJS does not support this as a built-in feature, but perhaps they might include it in a future release. Please review my code example for such a directive and point out any areas for improvement or if there's a better implementation.

Note: This example assumes that the view model (not shown) contains an array of items and a function called doSomething.

Update: Here is a working example in JSBin to illustrate its use.

Markup

<ul evt-delegate="doSomething(item)" evt-selector="li">
  <li ng-repeat="item in items">{{item.name}}</li>
</ul>

Code

(function () {
  angular.module('myModule').directive('evtDelegate', ['$parse', delegateDirective]);

  function delegateDirective($parse) {
    return {
      restrict: 'A',
      scope: {
        selector: '='
      },
      link: link
    };

    function link(scope, element, attributes) {
      var selector = attributes.evtSelector,
          expression = attributes.evtDelegate,
          expressionHandler = $parse(expression);

      element.on('click', function (e) {
        var targetNode, targetScope;

        targetNode = getTargetNode(element[0], e.target, selector);

        if (targetNode) {
          targetScope = angular.element(targetNode).scope();

          targetScope.$apply(function () {
            expressionHandler(targetScope);
          });
        }
      });
    }

    function getTargetNode(parent, child, selector) {
      if (!parent || !child) {
        return null;
      }

      if (!selector) {
        return child;
      }

      if (parent === child) {
        // we traveresed all the way up to the parent node, so return
        // null since the selector does not exist as a child
        return null;
      }

      // we cannot trust that child.nodeName is uppercase, 
      // so we make sure it is before doing the comparison
      if (child.nodeName.toUpperCase() === selector.toUpperCase()) {
        return child;
      }

      return getTargetNode(parent, child.parentNode, selector);
    }
  }
})();
\$\endgroup\$
  • \$\begingroup\$ Greetings, awesome question, in my experience you will get the best feedback if you set up a working jsbin/fiddle/plnkr with this. \$\endgroup\$ – konijn Aug 14 '14 at 19:12
  • \$\begingroup\$ Also, cant you just let the ng-click bubble up to the ul level ? \$\endgroup\$ – konijn Aug 14 '14 at 19:14
  • \$\begingroup\$ @konijn A JSBin example has been added (see above). The point of event delegation is to not need an ng-click directive on each <li> element. \$\endgroup\$ – Brett Aug 14 '14 at 19:38
1
\$\begingroup\$

I am puzzled by how you use item both inside and outside ng-repeat.

Each iteration of ng-repeat has its own scope, so its own item. For instance, if items = [item1, item2], which item do you mean to use inside doSomething? The way it is used now, the item seen by that function will be another item on the outside scope. If this is the intention, this variable should be named differently:

<ul evt-delegate="doSomething(outsideItem)" evt-selector="li">
  <li ng-repeat="item in items">{{item.name}}</li>
</ul>

UPDATE. Having another look I see better what you are trying to achieve. Still I would improve design by making the directive more re-usable.

You basically want to throw any HTML inside the directive and pick the model associated with event's target. The directive makes this model available on the scope of your expression doSomething(model). Just like $event made available inside the ng-click. I would then mark this model as something special like:

<ul evt-delegate="doSomething($my-target-model)">

Then it is the job of the directive to pick the right model and make it available under this name. This way I can change item to my-item inside ng-repeat without breaking things, so the directive is more encapsulated and re-usable.

\$\endgroup\$
  • \$\begingroup\$ I admit it seems confusing, but item does not refer to anything until the event occurs and the expression is parsed by $parse in the link function of the directive. Only then is the scope determined and only then does item refer to the element in the items collection that was clicked on. \$\endgroup\$ – Brett Aug 18 '14 at 13:20
  • \$\begingroup\$ The JSBin example does work. I'm not sure what issue you are having with it. \$\endgroup\$ – Brett Aug 18 '14 at 13:21
  • \$\begingroup\$ Oh, I see what you are trying to achieve. But can't you just use jQuerys event delegation? For some reason, your JSBin had auto-run disabled, after I enabled it, it works. \$\endgroup\$ – Dmitri Zaitsev Aug 18 '14 at 13:46
  • \$\begingroup\$ Yea, I could, but I'm avoiding jQuery and attempting to use an AngularJS-only implementation. \$\endgroup\$ – Brett Aug 18 '14 at 14:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.