Where developers come to connect, share, build and be inspired.

63

'Safe' $apply in Angular.JS

63550 views


If you find yourself triggering the '$apply already in progress' error while developing with Angular.JS (for me I find I hit most often when integrating third party plugins that trigger a lot of DOM events), you can use a 'safeApply' method that checks the current phase before executing your function. I usually just monkey patch this into the $scope object of my topmost controller, and Angular is nice enough to propagate it throughout the rest of my application for me:

$scope.safeApply = function(fn) {
  var phase = this.$root.$$phase;
  if(phase == '$apply' || phase == '$digest') {
    if(fn && (typeof(fn) === 'function')) {
      fn();
    }
  } else {
    this.$apply(fn);
  }
};

And then just replace $apply with safeApply wherever you need it

$scope.safeApply(function() {
  alert('Now I'm wrapped for protection!');
});

Comments

  • User-avatar

    But this is bad :). Don't do that.

  • Ef820271b3f3e060aea24c2484602ef1

    An easier way is to wrap the function in an $timeout without delay:

    $timeout(function(){ // Do something });

  • User-avatar

    Here's safeApply as an Angular service you can attach to your module. Additionally, this version accounts for calls to $apply() that don't pass in a function. To use it, attach the following to your module:

    .factory('safeApply', [function($rootScope) {
        return function($scope, fn) {
            var phase = $scope.$root.$$phase;
            if(phase == '$apply' || phase == '$digest') {
                if (fn) {
                    $scope.$eval(fn);
                }
            } else {
                if (fn) {
                    $scope.$apply(fn);
                } else {
                    $scope.$apply();
                }
            }
        }
    }])
    

    and access it with dependency injection:

    .controller('MyCtrl', ['$scope,' 'safeApply', function($scope, safeApply) {
        safeApply($scope);                     // no function passed in
        safeApply($scope, function() {   // passing a function in
        });
    }])
    
  • 9f6fde47d60d9e383ac1c492c4059358

    Doing this with a service (as suggested above @andrewreutter) comes far too late for some use cases. Be a cool cat on the block, and use a decorator to configure this VERY early in your applications bootstrap phase before any module's services, directives, etc may try to access it before its available :)

    yourAwesomeModule.config([
      '$provide', function($provide) {
        return $provide.decorator('$rootScope', [
          '$delegate', function($delegate) {
            $delegate.safeApply = function(fn) {
              var phase = $delegate.$$phase;
              if (phase === "$apply" || phase === "$digest") {
                if (fn && typeof fn === 'function') {
                  fn();
                }
              } else {
                $delegate.$apply(fn);
              }
            };
            return $delegate;
          }
        ]);
      }
    ]);
    
  • 638fa0a11d0cb897a6dab71bfdcacf49

    Angular's apply is able to run without a function as a parameter, so it will propagate all previous changes to the UI. I purpose this update: $scope.safeApply = function(fn) { var phase = this.$root.$$phase; if(phase == '$apply' || phase == '$digest') { if(fn) fn(); } else { this.$apply(fn); } };

  • User-avatar

    @roypeled Thanks for the note, I hadn't realized apply could be called without a parameter. I've updated by original code to take that into account.

  • D2f0d2b132885be6fcf53d715e16920e

    Have you considered submitting this to core?

  • User-avatar

    @digger69 I have considered it and have been looking through repository to see where it would best fit. I'll update this thread once I submit a pull request.

  • User-avatar

    @marklagendijk That will likely work the vast majority of the time, though, in my experience, if you're responding to tons of events (such as a video feed), it still has the potential to result in multiple digests/applies.

    More importantly, it results in your code being called after the current process thread. For many situations that may be preferable (particularly when doing DOM binding, such as with a jQuery plugin), but if you intend to immediately use any of the variables you just worked on, it will cause problems.

  • 55d7fb2e6e2c49f5bfa2bf873eb06d09

    That example wins.

  • 0_-rftk-geptzy2r8truxtkkhi1qa1m98trm1oklrr2algltb-yjcsoakzjslddaty14lxx9mnh8xg

    Genius!

  • Ea64919479517eee432ed2b8f7dbc695

    Question: Why use this.$root instead of just using $rootScope? Is there a difference? That way I can write a service that is independent of the current $scope.

  • User-avatar

    There could be a race condition in here... What if my function execution ends after the current .$apply has called $diggest in order to refresh the scope? This is the case when we call .safeApply when something is already happening on .$apply, as you can see on the conditional, it will just simply execute the function.

  • 20120725_123421

    You should almost always know if you're in AngularJS context or not, so this should be used only in some corner cases.

    Triggering digest cycle too often won't do good for your app's performance. Lots of operations can and should be done outside (like scrolling / resizing handlers)

  • None

    What if in function callback that has been provided to safeApply contains some $scope changes that angular already digested it. And at the meantime not all the scope variable digest has been finished yet. So in that case we will have (phase == '$apply' || phase == '$digest') == true and the changes in function provided will not take effect. Maybe I miss something.

Add a comment