AngularJS - why is $apply required to properly resolve a $q promise? - angularjs

I'm trying to write a small service in my angular app which will enable me to pick config parameters specified in global Javascript objects. I don't want to attempt accessing the global config object unless the document is ready (because I cannot guarantee the order in which script elements will be inserted in the HTML).
However, I can't understand why I need the call to $apply for the resolution to actually propagate to the then callback.
myModule.service('GlobalConfigService', ['$q', '$rootScope', function($q, $rootScope) {
var def = $q.defer();
$(document).ready(function() {
def.resolve(MyConfig.val);
$rootScope.$apply();
});
def.promise.then(function () {
console.log('I am called only when $apply() is called. Why?');
});
return def.promise;
}]);

In AngularJS the results of resolve() are propagated asynchronously, inside a $digest cycle, not immediately. This means that callbacks registered with then() will only be called (later) when a digest cycle occurs.
In your code, nothing is causing Angular to enter a digest cycle, so the then() callback is never called. Calling $apply() is one way to cause a digest cycle to run. Another way: add a button with ng-click that does nothing, then click that, it will cause a digest cycle and you should see the results:
<button ng-click="">Force digest by clicking me</button>
See also https://stackoverflow.com/a/14657974/215945

Related

How to test a method which returns a promise in sinon?

I have the following controller:
app.controller('SearchVideosController',
function SearchVideosController($scope, videoRepository) {
$scope.DoSearch(id, text) {
// Do some work...
videoRepository.getVideosForUserBasedOnSearchText(id,text)
.then(function(data){
// Do something with the data.
});
};
};
My videoRepository.getVideosForUserBasedOnSearchText() method uses $q and I want to create stub to ensure that the call is made.
I tried :
it("should have 3 searched videos", function(){
...
mockVideoRepository.getVideosForUserBasedOnSearchText.returns([]);
but get .then() is undefined.
Not sure how to handle the then() call.
You would need to get hold of $q service instance and use $q.when to create a promise wrapped value:-
mockVideoRepository.getVideosForUserBasedOnSearchText.returns($q.when([]));
Also remember you would need to manually perform a digest cycle in your test before the expecation to evaluate the result of the getVideosForUserBasedOnSearchText call. Only when a digest cycle is invoked promise will be resolved in your test. You can do it by getting hold of scope, and perform $digest or $apply. Example:- rootScope.$apply()

Angular Watch and ng-click sequence of events

I have this code inside an angular directive, and I'm finding the $watch behavior a bit confusing. The updateSelect is called in an "ng-click":
scope.updateSelect = function (type) {
scope.selectionCtrl.activeList = scope.seedLists[type];
scope.selectionCtrl.activeListKey = type;
scope.selectionCtrl.activeSelection = scope.selection[type];
scope.selectionCtrl.staged = [];
scope.selectionCtrl.stageRemove = [];
if (type !== scope.activeTab) {
scope.activeTab = type;
}
console.log("update");
};
scope.$watch('selectionCtrl.activeList', function(newValue, oldValue) {
console.log("watch");
}, true);
When I click on the button (triggering updateSelect), and watch the console, I see "update" and then "watch". The first thing that happens inside the function is selectionCtrl.activeList is set, so I would expect to see "watch" and then "update".
Shouldn't watch trigger as soon as the array has changed?
The function has to finish first as javascript is single threaded.
Because the function was called via the ng-click directive, angular will run a digest cycle. Part of that digest cycle is to run through the watch list and resolve all the changes that may have occurred since the cycle last ran.
In the example you give, selectionCtrl.activeList is changed in updateSelect which subsequently results in the watch callback being called.
When does Angular execute watch callback?
It's related to $digest and $apply, and certainly it does not execute within your raw javascript code.
To make watch execute forcefully, you can run $scope.apply() manually, but may cause more problem and not necessary if it is within a angularjs function, i.e. $timeout, $interval, etc, because it will be called automatically after the function.
For more info., lookup;
How do I use $scope.$watch and $scope.$apply in AngularJS?
https://groups.google.com/forum/#!topic/angular/HnJZTEXRztk
https://docs.angularjs.org/api/ng/type/$rootScope.Scope :
The watchExpression is called on every call to $digest() and should return the value that will be watched. (Since $digest() reruns when it detects changes the watchExpression can execute multiple times per $digest() and should be idempotent.)
If you try i.e.:
scope.selectionCtrl.activeList = scope.seedLists[type];
scope.$digest();
you'll get Error: [$rootScope:inprog] $apply already in progress.

DOM not updated after scope modification in handler of async service within directive

My directive calls some async service based on $http. Service name comes to derectiva as a parameter:
.directive('lookup', ['$scope', function($scope, svc) {
var injector = angular.injector(['app', 'ng']);
var svc = injector.get(scope.serviceName);
...
$scope.clickHandler = function () {
svc.lookup($scope.inputText).then(function (res) {
$scope.inputText = res.data.name;
});
}
...
})
While setting chrome breakpoint inside then() handler we can see by call stack that it's fired within $apply(), so every scope change should be reflected in DOM. $q documentation also states that handlers should be synchronized with scope/dom update cycle, but my view is not updated after clickHandler(). Calling explicit scope.$digest() after scope change does the thing, but don't know why it does not work withouth it. Another strange thing I've noticed is that on the breakpoint $scope has no parent-child relation with $rootScope for which http response handler is wrapped by $apply(). Maybe this is the reason of my troubles. Am I doing something wrong?
Real code is hard to cite, but see essential example code on plunker
Explanation:
The villain of the piece is the use of angular.injector in the link function:
var injector = angular.injector(['ng', 'app']);
var lookupService = injector.get(scope.serviceName);
This code will create a new injector function instead of retrieving the one already created for your application when it was bootstrapped.
Services in Angular are singletons in the sense that they are only created once per injector. In your case this means that injector.get(scope.serviceName) will return a new instance of the service and not the same instance as you possibly would've interacted with before.
The same goes for the $http service that your lookupService uses. In turn, $http injects $rootScope to trigger the digest cycle by calling $apply on it after a XHR request. But as the $rootScope will not be the same as for the rest of your application, the digest cycle will be in vain.
This is also the reason for the behaviors you are witnessing, and why an explicit call to scope.$digest() helps, as it will trigger the digest cycle on the correct scope chain.
Solution:
Inject the $injector service in your directive and use that to retrieve the service you want:
.directive('lookup', ['$injector',
function($injector) {
function link(scope, elem) {
var lookupService = $injector.get(scope.serviceName);
Demo: http://plnkr.co/edit/nubMohsbHAEkbYSg39xV?p=preview

How to be aware that when a function is run in angular context?

Say I have an service:
angular.module("app").factory("myService", function($rootScope){
return {
doSomething: function(){
console.log('doingSomething', $rootScope.$$phase);
$rootScope.someVar = true;
}
}
});
If I run it inside the controller like this
angular.module("app").controller("HomeController", function(myService){
myService.doSomething();
});
The console log gives:
doingSomething $apply
But if I run the service inside a unit test environment
it('should doSomething', inject(function(myService) {
myService.doSomething();
}));
The console log only get
doingSomething null
The typical answer that if you need to trigger the $digest cycle yourselves through $apply(). The angular js wiki mentioned about ng-click, $timeout and $http, but surely there are other places, such as running inside the controller. How can I determine that without trial and error?
It really depends on what you are testing. If you are testing something that requires a digest cycle (like $broadcast or $on events), then you will need to call scope.$apply() or simply scope.$digest() in the setup of the test.
However, most of the time, this is not required because services are usually designed as discrete pieces of functionality that do not require the scope. In the case of an HTTP call, the $httpBackend` service allows you to mock out the response.
For ng-click, you shouldn't be using this is a service. And for a controller you'll be binding to a function that you can test in isolation.
Hope this helps.

angularjs - promise never resolved in controller

In my controller I'm getting a promise from another service. I add a 'then' clause to it, but the 'then' is never called.
See this plunker: http://plnkr.co/edit/dX0Oz1?p=preview (javascript version)
'fakeLongRunningPromise' creates a promise that resolves itself after 2 seconds.
In the controller itself I send a note to the console once the promise is resolved.
I can tell that the promise is being resolved because "Resolving promise" it outputted to the console. Why doesn't it output "promise resolved"?
Thinking maybe the promise is going 'out of scope' because the controller returns?
The AngularJS the result of promises resolution is propagated asynchronously, inside a $digest cycle. So, the callbacks registered with then will be only called upon entering the $digest cycle. The setTimeout executes "outside of the AngularJS world", and as such will not trigger callbacks.
The solution is to use Scope.$apply or the $timeout service. Here is the version with $apply:
window.setTimeout(function() {
console.log("Resolving promise");
$scope.$apply(function(){
deffered.resolve("worked");
});
}, 2000);
Here is a fixed plunk (JavaScript): http://plnkr.co/edit/g5AnUK6oq2OBz7q2MEh7?p=preview
I've used $timeout instead of setTimeout and it works:
# Resolve the promise after 2 seconds
$timeout( ()->
console.log "Resolving promise"
deffered.resolve ("worked")
, 2000)

Resources