2
votes

I want a directive that works with a list of object but I also need to accept either an 2-way bind or a function bind.

app.directive('myDir', function() {
    return {
        scope: {
            list: "=?",
            list_func: "&?listFunc"
        },

        controller: ['$scope', function($scope) {
            $scope.get_list = function() {
                if($scope.list !== undefined)
                    return $scope.list();

                if($scope.list_func !== undefined)
                    return $scope.list_func()();
            };
        }]
    };
});

However when I use the listFunc attribute with a function that returns a list, I get this error:

VM607 angular.js:68 Uncaught Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting! Watchers fired in the last 5 iterations: [[{"msg":"fn: regularInterceptedExpression","newVal":19,"oldVal":17},{"msg":"fn: regularInterceptedExpression","newVal":"Harry"},{"msg":"fn: regularInterceptedExpression","newVal":"65"},{"msg":"fn: regularInterceptedExpression","newVal":"Sally"},{"msg":"fn: regularInterceptedExpression","newVal":"66"},{"msg":"fn: regularInterceptedExpression","newVal":9,"oldVal":8},{"msg":"fn: regularInterceptedExpression","newVal":"val"},{"msg":"fn: regularInterceptedExpression","newVal":"1"}],[{"msg":"fn: regularInterceptedExpression","newVal":21,"oldVal":19},{"msg":"fn: regularInterceptedExpression","newVal":"Harry"},{"msg":"fn: regularInterceptedExpression","newVal":"65"},{"msg":"fn: regularInterceptedExpression","newVal":"Sally"},{"msg":"fn: regularInterceptedExpression","newVal":"66"},{"msg":"fn: regularInterceptedExpression","newVal":10,"oldVal":9},{"msg":"fn: regularInterceptedExpression","newVal":"val"},{"msg":"fn: regularInterceptedExpression","newVal":"1"}],[{"msg":"fn: regularInterceptedExpression","newVal":23,"oldVal":21},{"msg":"fn: regularInterceptedExpression","newVal":"Harry"},{"msg":"fn: regularInterceptedExpression","newVal":"65"},{"msg":"fn: regularInterceptedExpression","newVal":"Sally"},{"msg":"fn: regularInterceptedExpression","newVal":"66"},{"msg":"fn: regularInterceptedExpression","newVal":11,"oldVal":10},{"msg":"fn: regularInterceptedExpression","newVal":"val"},{"msg":"fn: regularInterceptedExpression","newVal":"1"}],[{"msg":"fn: regularInterceptedExpression","newVal":25,"oldVal":23},{"msg":"fn: regularInterceptedExpression","newVal":"Harry"},{"msg":"fn: regularInterceptedExpression","newVal":"65"},{"msg":"fn: regularInterceptedExpression","newVal":"Sally"},{"msg":"fn: regularInterceptedExpression","newVal":"66"},{"msg":"fn: regularInterceptedExpression","newVal":12,"oldVal":11},{"msg":"fn: regularInterceptedExpression","newVal":"val"},{"msg":"fn: regularInterceptedExpression","newVal":"1"}],[{"msg":"fn: regularInterceptedExpression","newVal":27,"oldVal":25},{"msg":"fn: regularInterceptedExpression","newVal":"Harry"},{"msg":"fn: regularInterceptedExpression","newVal":"65"},{"msg":"fn: regularInterceptedExpression","newVal":"Sally"},{"msg":"fn: regularInterceptedExpression","newVal":"66"},{"msg":"fn: regularInterceptedExpression","newVal":13,"oldVal":12},{"msg":"fn: regularInterceptedExpression","newVal":"val"},{"msg":"fn: regularInterceptedExpression","newVal":"1"}]] http://errors.angularjs.org/1.6.2/$rootScope/infdig?p0=10&p1=%5B%5B%7B%22ms…2fn%3A%20regularInterceptedExpression%22%2C%22newVal%22%3A%221%22%7D%5D%5D at VM607 angular.js:68 at Scope.$digest (VM607 angular.js:17893) at Scope.$apply (VM607 angular.js:18125) at done (VM607 angular.js:12233) at completeRequest (VM607 angular.js:12459) at XMLHttpRequest.requestLoaded (VM607 angular.js:12387)

I created this plunker example (with an open a javascript console you can see these errors) demonstrating what I need. In this example I get these errors but the view still gets updated. On the app I'm developing (which is much bigger) I get so many $digest errors that the site slows down and the view doens't get updated.

What is the best way of binding a function without ending in an endless loop?

1
Expression & binding should only be used to communicate events to a parent controller. Use one way < to get data from a parent controller. Avoid two-way = binding.georgeawg
@georgeawg thanks for the comment. I avoid two-way binding as much as possible, in my case the input list changes slightly depending on the interaction of the user with the directive.Pablo

1 Answers

5
votes

Angular registers an implicit $watchCollection on the get_list() expression. This expression will get called at least twice because angular wants to check if the model has stabilized.

<ul>
  <li ng-repeat="obj in get_list()">
    name: {{ obj.name }}<br />
    age: {{ obj.age }}
  </li>
</ul>

Your code returns a different array of objects everytime get_list() is called so angular continues digesting until it reaches the max digest cycles limit.

  $scope.get_list = function(val) {
    return function() {
      return [ { name: "val", age: val } ];
    }
  }

Even though the content is the same, the function creates new objects each time it is called.

Avoid using function calls in templates. Instead put values on scope and have the directive react to changes in those values.

From the Docs:

Error: $rootScope:infdig

This error occurs when the application's model becomes unstable and each $digest cycle triggers a state change and subsequent $digest cycle. AngularJS detects this situation and prevents an infinite loop from causing the browser to become unresponsive.

One common mistake is binding to a function which generates a new array every time it is called. For example:

<div ng-repeat="user in getUsers()">{{ user.name }}</div>
$scope.getUsers = function() {
    return [ { name: 'Hank' }, { name: 'Francisco' } ];
};

Since getUsers() returns a new array, AngularJS determines that the model is different on each $digest cycle, resulting in the error. The solution is to return the same array object if the elements have not changed:

var users = [ { name: 'Hank' }, { name: 'Francisco' } ];

$scope.getUsers = function() {
    return users;
};

— AngularJS Error Reference - Error: $rootScope:infdig


Should I give up with this kind of functions and use a filter instead?

TL;DR Yes.

Components should just act as "skins". They should only display the data given to them and communicate user events to the parent controller. The Model should be the Single Source of Truth. Directive should not need to fetch data from a parent controller.

Expression & binding should only used to communicate user events to the parent controller. The parent controller should then change the Model appropriately. This Separation of Concerns makes for an app that is more easily understood, debug, tested, and maintained.


One more question: this also applies for functions returning functions, right?

The "functions returning functions" pattern should be avoided. It makes for a confusing template. Instead invoke functions with locals:

<my-component on-event="gotEvent($event)">
</my-compnent>
scope: { onEvent: "&" },
template: `<input ng-model=myModel ng-change="myChange()" />`,
link: function(scope, elem, attrs) {
    scope.myChange = function() {
        scope.onEvent({$event: myModel});
    };
} 

I recommend prefixing local variable with $ to differentiate them from parent scope variable. Some people recommend using $event to conform to the conventions of Angular 2+.

From the Docs:

  • & or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given <my-component my-attr="count = count + $value"> and the isolate scope definition scope: { localFn:'&myAttr' }, the isolate scope property localFn will point to a function wrapper for the count = count + $value expression. Often it's desirable to pass data from the isolated scope via an expression to the parent scope. This can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment($amount) then we can specify the amount value by calling the localFn as localFn({$amount: 22}).

— AngularJS Comprehensive Directive API Reference ($scope)