Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why $scope.apply matters for angular's $q?

Tags:

angularjs

I am following the help on AngularJS's documentation for Q's implementation $q. I tried out the following code from https://docs.angularjs.org/api/ng/service/$q

// for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet`
  // are available in the current lexical scope (they could have been injected or passed in).

  function asyncGreet(name) {
    var deferred = $q.defer();

    setTimeout(function() {
      // since this fn executes async in a future turn of the event loop, we need to wrap
      // our code into an $apply call so that the model changes are properly observed.
      scope.$apply(function() {
        deferred.notify('About to greet ' + name + '.');

        if (okToGreet(name)) {
          deferred.resolve('Hello, ' + name + '!');
        } else {
          deferred.reject('Greeting ' + name + ' is not allowed.');
        }
      });
    }, 1000);

    return deferred.promise;
  }

  var promise = asyncGreet('Robin Hood');
  promise.then(function(greeting) {
    alert('Success: ' + greeting);
  }, function(reason) {
    alert('Failed: ' + reason);
  }, function(update) {
    alert('Got notification: ' + update);
  });

My understanding is that the $scope.apply here is to give the callback Angular's context and make sure variables under $scope are accessible.

But below on comparing $q and Kris Kowal's Q, the test code goes:

it('should simulate promise', inject(function($q, $rootScope) {
   var deferred = $q.defer();
   var promise = deferred.promise;
   var resolvedValue;

   promise.then(function(value) { resolvedValue = value; });
   expect(resolvedValue).toBeUndefined();

   // Simulate resolving of promise
   deferred.resolve(123);

   // Note that the 'then' function does not get called synchronously.
   // This is because we want the promise API to always be async, whether or not
   // it got called synchronously or asynchronously. 
   expect(resolvedValue).toBeUndefined(); // <= so the deferred is not resolved without the 'apply'?

   // Propagate promise resolution to 'then' functions using $apply().
   $rootScope.$apply();
   expect(resolvedValue).toEqual(123);
 }));

It says $rootScope.$apply() is to propagate the promise resolution to 'then'. I am confused there... So without using apply, the deferred.resolve will not actually resolve?

like image 317
lkahtz Avatar asked Jan 11 '23 15:01

lkahtz


1 Answers

This documentation sucks.

So the thing with $q is that when your promise is resolved (and also presumably on reject or notify), it invokes your handlers within $rootScope.evalAsync which ensures that after invocation, it'll trigger a digest and thus the rest of your application can have a chance to update or otherwise respond to the changes, just the way we like it.

As you found out, it works just fine without the explicit $apply in an example app. However, the reason why they are doing explicit $apply is because that automagic with $evalAsync doesn't get a chance to work when running synchronously in a test, not because it's required for your application to Just Work ™.

With a few other notable services that are augmented for testing in angular-mock.js, like $http and $timeout, we can explicitly flush when we want to simulate an async http request/response or a timeout (for example). The equivalent to stuff waiting to be evaled is to trigger a digest, which will get your promise handler invoked in the proper context. This is done with $apply or $digest, and hence why you're seeing it in their examples... because their examples are written as synchronous tests.

The docs should explain the difference between what you need to do to get your tests working and what your application itself should focus on to get the job done. The docs have a bad habit of making test facts their examples, and it just confuses people.

Hope that helps.

like image 175
moribvndvs Avatar answered Jan 21 '23 06:01

moribvndvs