Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

solving $rootScope:infdig Infinite $digest Loop

I get the basic idea of the infinite digest loop and how it happens, but I'm running into the problem. Here is a fiddle demonstrating my code and problem:

http://jsfiddle.net/eS5e5/1/

In the jsfiddle console you'll see the infinite digest loop.

Basically I have to make decisions on data that may not have loaded yet so I need to wait for the promise to resolve using then(). I have a promise called user. There are two different places in the code where I call then() on user.

  1. Just after I define it. I need to set a scope variable based on it.
  2. In another scope method, $scope.isAdmin()

For number 2, it might be asked why I just don't use $scope.user directly in the $scope.isAdmin() method. The problem is, it's possible for $scope.isAdmin() to be called before the async request for the user returns, in which case I need to 'block' before returning from $scope.isAdmin().

My question is, what about $scope.isAdmin() is making angular think that a 'watched' variable has changed and that the digest cycle needs to run again?

$scope.isAdmin() isn't actually changing anything.

Here is the stripped down code:

HTML:

<body ng-controller='myController'>  
  <div ng-if='isAdmin()'>Hi! <strong>{{ user.username }}</strong> is an Admin!!!</div>
  <div ng-if='!isAdmin()'>Hi! <strong>{{ user.username }}</strong> is NOT an Admin!!!</div>
</body>

And the JS:

angular.module('myApp', [])
  .factory('myService', function($q, $timeout) {
    return {        
      getUser: function() {
        var deferred = $q.defer();

        $timeout(function() {
          deferred.resolve({ username: 'me', isAdmin: true });
        }, 2000);

        return deferred.promise;
      }
    };
  })
  .controller('myController', function($scope, $q, myService) {      
    var getUserDeferred = $q.defer();
    var user = getUserDeferred.promise;
    user.then(function(user) {
      $scope.user = user;
      return user;
    });

    $scope.getUser = function() {
      return myService.getUser().then(function(user) {
        getUserDeferred.resolve(user);
      });
    };

    $scope.isAdmin = function() {
      return user.then(function(user) {
        return user.isAdmin;
      });
    };

    $scope.getUser();
  });
like image 473
lostdorje Avatar asked Jul 09 '14 09:07

lostdorje


People also ask

What is $scope and $rootScope?

"$rootScope” is a parent object of all “$scope” angular objects created in a web page. $scope is created with ng-controller while $rootscope is created with ng-app . Follow this answer to receive notifications.

What is use of $rootScope?

Root Scope All applications have a $rootScope which is the scope created on the HTML element that contains the ng-app directive. The rootScope is available in the entire application. If a variable has the same name in both the current scope and in the rootScope, the application uses the one in the current scope.

How many $rootScope an Angularjs application can have?

An app can have only one $rootScope which will be shared among all the components of an app. Hence it acts like a global variable. All other $scopes are children of the $rootScope.

What is a digest cycle?

Digest cycle is what Angular JS triggers when a value in the model or view is changed. The cycle sets off the watchers which then match the value of model and view to the newest value. Digest cycle automatically runs when the code encounters a directive.


1 Answers

So I finally figured out my own problem and thought I would answer it for others in case someone else may find this info useful.

The crux of the fix had to do with 2 concepts: angular promises and angular watches. By being aware of and applying the 2 concepts together the fix was actually pretty simple.

Everything you put on $scope is 'watched' including functions. Every time something watched changes $scope.$apply() runs again to apply the changes. If a scope function (eg: $scope.isAdmin()) changes its return value from one 'apply' to the next it will trigger another 'apply', until things stabilize and the return value isn't changing.

But in my code I was returning user.then(...) which just returns a new promise (which kept the apply cycle going on forever since the return value kept changing).

In my isAdmin() function I needed to defer its return value until the user actually loaded (any other return value would be meaningless). So I changed the code to check if the user async call had resolved by checking $scope.user and if so returning a valid isAdmin value. If $scope.user was still not defined I would just return the promise I already created.

I changed the $scope.isAdmin() to be:

$scope.isAdmin = function() {
  if ($scope.user) {
    return $scope.user.isAdmin;
  }

  return user;
};

This has the same effect as the original code without triggering an infinite apply cycle. Specifically, if the $scope.user has not resolved we still return a promise as before, by returning the user var. Note however that the user var is the same promise not a new one created by then() so the apply cycle stabilizes.

And just for completeness here is the updated jsfiddle:

http://jsfiddle.net/eS5e5/2/

like image 65
lostdorje Avatar answered Nov 25 '22 19:11

lostdorje