Why $scope.apply matters for angular's $q? - 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?

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.

Related

Karma Angular Unit test a promise never being resolved

I am trying to ensure my unit test case covers the scenario of a promise never being resolved, and I am having some issues.
A code block is worth a thousand words, so..
it('returns my resource', function() {
var myUser = {name:'Bob'}
$httpBackend.expectGET('/myresource').respond(200,myUser);
myService.getMyUser()
.then(function(data) {
expect(data).toEqual(myUser);
},fail);
});
This is all well and good, and tests that the response is as expected and also fails the test should the promise be rejected.
However, should the promise never be resolved at all, the test passes and I would like it to fail. I am caching this request in myService like so:
var cachedUser;
function getMyUser(forceUpdate) {
var deferred = $q.defer();
if (cachedUser && !forceUpdate) {
deferred.resolve(cachedUser);
} else {
$http.get('/myresource')
.then(function(data) {
cachedUser = data;
deferred.resolve(data);
},function(error) {
deferred.reject(error);
});
}
return deferred.promise;
}
Now in the above scenario, if one were to remove the line "deferred.resolve(data)" from within the $http.get, the test would still pass. This is because the callback function containing the expectation for data.toEqual(myUser) is never run. However the test should fail because removing that line breaks the purpose of this function.
I have tried within the test code to make the positive callback a spy and expect the spy toHaveBeenCalled, but that seems to run before the promise is resolved and fails even though I can see that the spy's function ran via a console log.
I have also tried within the unit test putting the promise in a variable and then expect(promise.$$state.status).toEqual(1), but again it appears that this expectation is run before the promise is resolved and so the test fails when it should pass.
Please be sure you understand the problem before answering, post a comment if I have not been clear.
The issue turned out to be that when dealing with $httpBackend, you must flush() the backend for requests to go through. I was doing this in an afterEach() block around every test, and so when trying to test the state of the promise from within the test block the promise had not actually been resolved yet.
By moving $httpBackend.flush() to within the unit test, the promise returned did indeed update its status from 0 to 1.
Here is the final unit test code:
it('returns my resource', function() {
var myUser = {name:'Bob'}
var promise;
$httpBackend.expectGET('/myresource').respond(200,myUser);
promise = myService.getMyUser()
.then(function(data) {
expect(data).toEqual(myUser);
},fail);
$httpBackend.flush();
expect(promise.$$state.status).toEqual(1);
});

$scope variable is undefined when it is set inside a function

I have the following example code in my learning app. The service does his job and pulls some data out of a page with json code generated by php, so far so good.
service:
(function() {
'use strict';
angular
.module('app.data')
.service('DashboardService', DashboardService);
DashboardService.$inject = ['$http'];
function DashboardService($http) {
this.getFormules = getFormules;
////////////////
function getFormules(onReady, onError) {
var formJson = 'server/php/get-formules.php',
formURL = formJson + '?v=' + (new Date().getTime()); // Disables cash
onError = onError || function() { alert('Failure loading menu'); };
$http
.get(formURL)
.then(onReady, onError);
}
}
})();
Then i call the getFormules function in my controller and put all the data inside my $scope.formuleItems and test if everything succeeded and 'o no'... $scope.formuleItems = undefined! - Strange because my view is showing data?
part of the controller:
dataLoader.getFormules(function (items){
$scope.formuleItems = items.data;
});
console.log('+++++++++++++++++', $scope.formuleItems); // gives undefined
The first thing i did was search around on stackoverflow to look if someone else had the same issue, and there was: Undefined variable inside controller function.
I know there are some walkarounds for this, i've done my own research, but something tells me that this (see example below) isn't the best way to solve this problem.
solution one: put $watch inside of the controller
$scope.$watch('formuleItems', function(checkValue) {
if (checkValue !== undefined) {
//put the code in here!
}
}
or even:
if($scope.formuleItems != null) {}
The rest of the controller is relying on $scope.formuleItems. Do i really have to put everything into that $watch or if? Can i fix this with a promise? I never did that before so some help would be appreciated.
The code in your callback
function (items){
$scope.formuleItems = items.data;
}
is evaluated asynchronously. That means you first fire the request, then javascript keeps on executing your lines of code, hence performs
console.log('+++++++++++++++++', $scope.formuleItems); // gives undefined
At this point the callback was not invoked yet, because this takes some time and can happen at any point. The execution is not stopped for this.
Therefore the value of $scope.formuleItems is still undefined, of course.
After that - at some not defined time in the future (probably a few milliseconds later) the callback will be invoked and the value of $scope.formuleItems will be changed. You have to log the value INSIDE of your callback-function.
You urgently have to understand this concept if you want to succeed in JavaScript, because this happens over and over again :)

What $q.defer() really does?

I'm learning about Angular JS and on the moment I'm trying to understand about promises and async programming and I have this doubt about $q.defer(). My point is the following: usually when people work with promises they do something like that, considering that $q is already available
function someAsyncFunction() {
var deferred = $q.defer();
/* Do things and if everything goes fine return deferred.resolve(result)
otherwise returns deferred.reject()
*/
return deferred.promise;
}
What is this really doing? When we do var deferred = $q.defer() it imediately switches all the execution of that function to another thread and return the promise being a reference to the results of this operation that is still performing there?
Is this the way we should think about when creating async methods?
With $q u run functions asynchronously.
Deferred objects signals that something, some task is done.
var defer = $q.defer(); // we create deferred object, which will finish later.
defer.promise // we get access to result of the deferred task
.then( // .then() calls success or error callback
function(param) {
alert("i something promised " + param);
return "something";
}); // u can use 1 or more .then calls in row
defer.resolve("call"); //returns promise
Here example:
http://jsfiddle.net/nalyvajko/HB7LU/29048/
Angular's $q service is based on the Javascript library Q. You can read more about it in the Q documentation, or read the code in the github repo. I think this part snipped from the introduction to the documentation explains it best:
If a function cannot return a value or throw an exception without
blocking, it can return a promise instead. A promise is an object that
represents the return value or the thrown exception that the function
may eventually provide. A promise can also be used as a proxy for a
remote object to overcome latency.

Backbone, Marionette, Jasmine: How to test jQuery deferred event

I'm very new to Jasmine and Marionette and looking for some help on how to test and even just the proper way to think about testing my application. Any pointers are welcome.
I have a Marionette Controller that I use to fetch my model, instantiate my views and render them. I use a method found at the bottom of this page so that the model is fetched before the view is rendered: https://github.com/marionettejs/backbone.marionette/blob/master/upgradeGuide.md#marionetteasync-is-no-longer-supported.
My controller method to fetch the model and display the view looks like so:
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
$.when(promise).then(_.bind(this.showContentView, this));
},
As you can see, it calls the showContentView after the model is fetched. That method is here:
showContentView: function(model){
App.views.Body = new bodyView({
model: App.models.Case
});
App.views.Body.on('case:update', this.submitCase, this);
// this.layout is defined in the controller's initialize function
this.layout.content.show(App.views.Body);
},
What is the proper way to test this functionality? I'd like to test the calling of the showContentView function after the completion of the promise. How should I break up the specs for this?
Thanks.
First, spy on your showContentView method and assert it has been called:
it('showCaseById', function (done) {
var controller = new Controller();
spyOn(controller, 'showContentView');
controller.showCaseById('foo');
expect(controller.showContentView).toHaveBeenCalledWith(jasmine.any(caseModel));
});
Secondly, I would recommend you stub out the call to fetch() so you don't hit the network, but it's starting to get a bit hairy now:
function caseModel() {
this.fetch = function () {
// return a promise here that resolves to a known value, e.g. 'blah'
};
}
Now, you can have a slightly stronger assertion, but this is a bit shonky because you're fiddling around with internals of your dependencies:
expect(controller.showContentView).toHaveBeenCalledWith('blah');
By overriding caseModel, when your controller method goes to create one, it gets your new version instead of the old one, and you can control the implementation of the new one just for this test.
There are ways to make this code more testable, but as it seems you're just starting out with testing I won't go into it all. You'll certainly find out those things for yourself as you do more testing.
First, it's important to understand that _.bind(fn, context) doesn't actually call fn. Instead, it returns a function that when called will call fn(). The context defines the object that fn will use internally as this.
It's not necessary but you could write showCaseById as :
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
var fn = _.bind(this.showContentView, this);
$.when(promise).then(fn);
},
As I say, that is unnecessary but now you understand that _.bind() returns a function and that $.when(promise).then(...) accepts a function as its (first) argument.
To answer the actual question, you can confirm that the App.models.Case.fetch() promise has been fulfilled by adding a further $.when(promise).then(...) statement, with a test function of your own choosing.
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
$.when(promise).then(_.bind(this.showContentView, this));
// start: test
$.when(promise).then(function() {
console.log("caseModel " + id + " is ready");//or alert() if preferred
});
// fin: test
},
The second $.when(promise).then(...) will not interfere with the first; rather, the two will execute sequentially. The console.log() satatement will provide reliable confirmation that the this.showContentView has been called successfully and that initial rendering should have happened.
If nothing is rendered at this point or subsequently, then you must suspect that this.showContentView needs to be debugged.

angularjs save changes after digest has finished

I think this might be quite common use-case with any angular app. I am simply watching some objects on my scope that are changed as part of several digest cycles. After digesting them (changing their values via databinding) has finished, I want to save them to databse.
A. Now, with the current solutions I see following problems:
running save in $timeout() - how to assure that save is called only
once
running a custom function in $scope.$evalAsync - how to find out what has been chaged
There are of course solutions to both of these prolblems, but non of those I know seem ehough elegant to me.
The question is: What is the most elegant solution to the problem?
B. In particular, what are the best practices to
make sure that save gets called only once in a digest cycle
find out that object is dirty after last digest
Here is a solution I've found working best for me - as an AMD modul. Inspired by Underscore.
/**
* Service function that helps to avoid multiple calls
* of a function (typically save()) during angular digest process.
* $apply will be called after original function returns;
*/
define(['app'], function (app) {
app.factory('debounce', ['$timeout', function ($timeout) {
return function(fn){ // debounce fn
var nthCall = 0;
return function(){ // intercepting fn
var that = this;
var argz = arguments;
nthCall++;
var later = (function(version){
return function(){
if (version === nthCall){
return fn.apply(that, argz);
}
};
})(nthCall);
return $timeout(later,0, true);
};
};
}]);
});
/*************************/
//Use it like this:
$scope.$watch('order', function(newOrder){
$scope.orderRules.apply(newOrder); // changing properties on order
}, true);
$scope.$watch('order.valid', function(newOrder){
$scope.save(newOrder); //will be called multiple times while digested by angular
});
$scope.save = debounce(function(order){
// POST your order here ...$http....
// debounce() will make sure save() will be called only once
});

Resources