$scope not updating when set from $http promise context - javascript

I'm trying to chain several $http calls, and then display the response of the last $http call. The calls all work correctly and the proper response is displayed, but only for the very first time I run the chain of calls. On subsequent runs, it makes it through the $http calls, but does not update the view with the latest response.
I can see in both Firefox and Chrome console network logs, that all the calls within the chain are getting executed (even on subsequent runs of the chain). In debugging the issue, I've replaced the $http calls with simple data assignment and everything works - including updates to the view - so at this point I'm mostly confident it's related to the $http/promise. Can anyone shed some light on what I'm doing wrong, or pointers on where to look as I start to read up on Angular.
Thanks.
I'm on Angular version 1.2.16. and here's my code:
//controller
function($scope, $http) {
$http.get('http://localhost:8080/call/one')
.then(function(responseOne) { return responseOne.data })
.then(function(dataFromCallOne) {
return $http.jsonp('http://localhost:8080/call/two')
.then(function(responseTwo) {
$scope.results = responseTwo.data
});
});
}
In my view, I have
<div ng-controller="MyCtrl">
{{results}}
<button ng-click="makeCall()">Call</button>
</div>
I've tried, the following but still no updates to my view, both methods work for initial display of results, but not on subsequent updates:
if(!$scope.$$phase) {
$scope.$apply()
}
and assigning chain results to $scope.result
$scope.results = $http.get('http://localhost:8080/call/one')
.then(function(responseOne) { return responseOne.data })
.then(function(dataFromCallOne) {
return $http.jsonp('http://localhost:8080/call/two')
.then(function(responseTwo) { return responseTwo.data });
});
$scope.results.then(function(data) {
$scope.results = data
});

I found my error. In my chain of $http calls, one of them is an $http.jsonp() call. When using JSONP, Angular requires a callback param - specifically called JSON_CALLBACK. This JSON_CALLBACK, then gets replace by an internally generated, unique callback name, right before the request is made.
?callback=JSON_CALLBACK --> ?callback=angular.callbacks._0
?callback=JSON_CALLBACK --> ?callback=angular.callbacks._1
?callback=JSON_CALLBACK --> ?callback=angular.callbacks._2
This was problematic for me since my jsonp request is to an OAuth provider that required signing the complete url (including the callback param). So initially I would sign the url with callback=JSON_CALLBACK, but Angular would turn it to something like callback=angular.callbacks._#. I would get an OAuth error since the signature and actual url didn't jive. Anyways, at some point I hard coded callback=angular.callbacks._0, just so I could get my OAuth client code working, but I forgot all about it.
Now subsequent calls to $http.jsonp() with non-unique callback params were not being processed after the response was returned. That's why my $scope is not updated after $http promise.
My hack at the moment is to continue using the hard coded callback=angular.callbacks._#, but with incrementing numbers.

Related

httpBackend Mock AJAX ES6 Promise in $q.when

I'm trying to mock a response to a JSONP GET request which is made with a function that returns an ES6 promise which I've wrapped in $q.when(). The code itself works just fine, however, in the unit tests the request is not being caught by $httpBackend and goes through right to the actual URL. Thus when flush() is called I get an error stating Error: No pending request to flush !. The JSONP request is made via jQuery's $.getJSON() inside the ES6 promise so I opted to try and catch all outgoing requests by providing a regex instead of a hard-coded URL.
I've been searching all over trying to figure this out for a while now and still have yet to understand what's causing the call to go through. I feel as if the HTTP request in the ES6 promise is being made "outside of Angular" so $httpBackend doesn't know about it / isn't able to catch it, although that may not be the case if the call was being made inside of a $q promise from the get-go. Can anyone possibly tell me why this call is going through and why a simple timeout will work just fine? I've tried all combinations of $scope.$apply, $scope.$digest, and $httpBackend.flush() here, but to no avail.
Maybe some code will explain it better...
Controller
function homeController() {
...
var self = this;
self.getData = function getData() {
$q.when(user.getUserInformation()).then(function() {
self.username = user.username;
});
};
}
Unit Test
...
beforeEach(module('home'));
describe('Controller', function() {
var $httpBackend, scope, ctrl;
beforeEach(inject(function(_$httpBackend_, $rootScope, $componentController) {
$httpBackend = _$httpBackend_;
scope = $rootScope.$new(); // used to try and call $digest or $apply
// have also tried whenGET, when('GET', ..), etc...
$httpBackend.whenJSONP(/.*/)
.respond([
{
"user_information": {
"username": "TestUser",
}
}
]);
ctrl = $componentController("home");
}));
it("should add the username to the controller", function() {
ctrl.getData(); // make HTTP request
$httpBackend.flush(); // Error: No pending request to flush !
expect(ctrl.username).toBe("TestUser");
});
});
...
For some reason this works, however:
it("should add the username to the controller", function() {
ctrl.getData(); // make HTTP request
setTimeout(() => {
// don't even need to call flush, $digest, or $apply...?
expect(ctrl.username).toBe("TestUser");
});
});
Thanks to Graham's comment, I was brought further down a different rabbit hole due to my lack of understanding several things which I will summarize here in case someone ends up in the same situation...
I didn't fully understand how JSONP works. It doesn't rely on XmlHttpRequest at all (see here). Rather than trying to fiddle with mocking responses to these requests through JSONP I simply switched the "debug" flag on the code I was using which disabled JSONP so the calls were then being made via XHR objects (this would fail the same origin policy if real responses were needed from this external API).
Instead of trying to use jasmine-ajax, I simply set a spy on jQuery's getJSON and returned a mock response. This finally sent the mocked response to the ES6 promise, but for some reason the then function of the $q promise object which resulted from wrapping the ES6 promise wasn't being called (nor any other error-handling functions, even finally). I also tried calling $scope.$apply() pretty much anywhere in the off chance it would help, but to no avail.
Basic implementation (in unit test):
...
spyOn($, 'getJSON').and.callFake(function (url, success) {
success({"username": "TestUser"}); // send mock data
});
ctrl.getData(); // make GET request
...
Problem (in controller's source):
// user.getUserInformation() returns an ES6 promise
$q.when(user.getUserInformation()).then(function() {
// this was never being called / reached! (in the unit tests)
});
Ultimately I used #2's implementation to send the data and just wrapped the assertions in the unit test inside of a timeout with no time duration specified. I realize that's not optimal and hopefully isn't how it should be done, but after trying for many hours I've about reached my limit and given up. If anyone has any idea as to how to improve upon this, or why then isn't being called, I would honestly love to hear it.
Unit Test:
...
ctrl.getData(); // make GET request
setTimeout(() => {
expect(ctrl.username).toBe("TestUser"); // works!
});

Calling $http promise inside controller

Thanks in advance for the help...
I have a controller I'm using to call an API to send a password reset link. I have abstracted the actual $http call off into a service to keep the controller thin. Originally I was doing something like this:
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
$http(request).then(function() {
return {}; //return some object using response
}]);
I felt like this would be the best approach as it would keep the controller as thin as possible and kept all service related actions separate. My problem with this was that returning from within the promise never actually returned me anything. I had to actually return the $http service call to get the promise. The only way I was able to make all of this work was if I called .then from within the controller.
//controller
angular.module('module')
.controller('forgotPasswordCtrl', ['forgotPasswordService', function(forgotPasswordService) {
forgotPasswordService.forgotPassword(emailAddress).then(function() {
//do stuff
}
}]);
//service
angular.module('module')
.service('forgotPasswordService', ['$http', function($http){
this.forgotPassword = function(emailAddress) {
return $http(request);
};
}]);
This just feels a little wrong to me because the controller now depends on receiving a promise back from the service. I may just be overthinking this but I would like a second opinion.
Is this considered acceptable/good practice? Is there an alternative to this which would allow me to achieve the encapsulation I'm looking for?
Thanks again.
I've interfaced with the $http from the controller in two slightly different ways.
Like you it felt wrong returning the $http from the service and interfacing with it.
So first I created services and passed in a success method and an error method (callbacks).
// Service
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
function sendForgotPasswordEmail(emailAddress, success, error){
$http.post('/api/v1/resetpassword', {emailAddress:emailAddress})
.then(success, error);
}
return {
sendForgotPasswordEmail: sendForgotPasswordEmail
}
}]);
// Controller
angular.module('module')
.controller('forgotPasswordCtrl', ['forgotPasswordService', function(forgotPasswordService) {
forgotPasswordService.sendForgotPasswordEmail(emailAddress,
function(response){ //success
// notify user of success
},
function(response){ // error
// notify user of error
});
}]);
This worked great. I created an large application this way, but as I started on my second large angular project I wondered why I was hiding the $http's promise?
By passing back the promise, I can use other libraries that support promises. With my first approach I can't leverage other libraries promise support.
Passing back the $http promise
// Service
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
function sendForgotPasswordEmail(emailAddress){
return $http.post('/api/v1/resetpassword', {emailAddress:emailAddress});
}
return {
sendForgotPasswordEmail: sendForgotPasswordEmail
}
}]);
// Controller
angular.module('module')
.controller('forgotPasswordCtrl', ['forgotPasswordService', function(forgotPasswordService) {
forgotPasswordService.sendForgotPasswordEmail(emailAddress)
.then(
function(response){ //success
// notify user of success
},
function(response){ // error
// notify user of error
});
}]);
I deleted my original answer, and I feel like a dork for stating that you could do it the other way. When I went back and checked my original code back when I first started angular, I found that I was calling then() twice in my application - once in my service where I returned the data, and once in my controller because calling $http(request).then() returns a promise.
The fact is, you're dealing with an asynchronous call. Suppose in your controller, you wanted to do this:
$scope.foo = myService.getFoo(); // No then()
The XmlHttpRequest inside the $http in getFoo() is an asynchronous call, meaning that it calls it and moves on. The synchronous option is deprecated. It's bad practice to make a blocking synchronous HTTP call because it will seize up your UI. This means you should use a callback when the data is ready, and the promise API is made just for that.
If you absolutely do not want to use the then() in your controller, I suppose you could probably pass your scope binding parameters to your service and let your service update them in your then call. I haven't tried this, and because it's asynchronous, I'm not sure if angular will know to call a digest() so you may need to call a $scope.$apply() if the values don't update. I don't like this, because I think the control of the values in the scope should be handled by the controller and not the service, but again - it's your personal preference.
Sorry for leading you astray with my initial answer - I ran into the same question you had, but when I looked back - I saw I used a silly solution.
-- Relevant statements in original answer --
Consider the following:
Where do you want your error handling for the call and who needs to know about it?
Do you need to handle specific failures in a particular controller or can they all be grouped together to one error handler? For some apps, I like to display the errors in a particular place rather than in a general modal dialog, but it's acceptable to create a service to handle all errors and pop them up for the user.
Where do you want to handle your progress/busy indicator?
If you have an interceptor wired up for all http calls and broadcasting an event to show/hide the busy indicator, then you don't need to worry about handling the promise in the controller. However some directives will use the promise to show a busy indicator which requires you to bind it to the scope in the controller.
To me, the decision is determined by the requirements and by personal choice.
Try using a callback like so:
angular.module('module')
.service('forgotPasswordService', ['$http', function($http) {
var recovery = function(request, callback) {
$http(request).then(function(response) {
callback(response);
})
}
return { recovery: recovery }
}]);
Then you would call it like this:
forgotPasswordService.recovery('http://...', function(response){
console.log(response);
})

The promise of a promise again (Angular JS)

Updated with HTTP and initial code based on requests/Please look at the bottom of the post:
I've been posting several questions on my AngularJS learning curve of late and the SO community has been fantastic. I've been a traditional C programmer when I used to program and have recently started writing my own ionic/Angular JS app. I'm struggling with the promise version of traditional async calls when it comes to converting a custom function to a promise. I don't think I really understood and I find various examples very contrived. I'd appreciate some help. I have some code which is not working, and I have some conceptual questions:
Let's take this simple function:
angular.module('zmApp.controllers').service('ZMDataModel', function() { return { getMonitors: function () { return monitors; } }
getMonitors is a simple function that basically returns an array of monitors. But here is the rub: When the app first starts, I call an http factory that does an http get and goes about populating this monitor list. This http factory is different from this service but invokes a setMonitor method in this service to populate the array. When the array is populated, a variable called 'monitorsLoaded' is set to 1. When this variable is set to 1, I know for sure monitors is loaded.
Now, I have a view with a controller called "MontageCtrl". I want to wait for the monitors to load before I show the view. In a previous post, one person suggested I use route resolve, but I had to first convert my getMonitors to a promise. So here is what I did:
angular.module('zmApp.controllers').service('ZMDataModel', function($q) {
getMonitors: function () {
var _deferred = $q.defer();
if (monitorsLoaded!=0)
{
console.log ("**** RETURNING MONITORS *****");
_deferred.resolve(monitors);
}
console.log ("*** RETURNING PROMISE ***");
return _deferred.promise;
},
Next up, in app.js I connected the route as follows:
.state('app.montage', {
data: {requireLogin:false},
resolve: {
message: function(ZMDataModel)
{
console.log ("Inside app.montage resolve");
return ZMDataModel.getMonitors();
}
},
Finally I modified my controller to grab the promise as such:
angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', function($scope,$rootScope, ZMHttpFactory, ZMDataModel,message) {
//var monsize =3;
console.log ("********* Inside Montage Ctrl");
It seems based on logs, I never go inside Montage Ctrl. Route resolve seems to be waiting for ever, whereas my logs are showing that after a while, monitorLoaded is being set to 1.
I have several conceptual questions:
a) In function getMonitors, which I crafted as per examples, why do people return a _deferred.promise but only assign a _deferred.resolve? (i.e. why not return it too?). Does it automatically return?
b) I noticed that if I moved var _deferred definition to my service and out of its sub function, it did work, but the next view that had the same route dependency did not. I'm very confused.
c) Finally I ready somewhere that there is a distinction between a service and a factory when it comes to route resolve as a service is only instantiated once. I am also very confused as in some route resolve examples people use when, and I am using .state.
At this stage, I'm deep into my own confusion. Can someone help clarify? All I really want is for various views to wait till monitorsLoaded is 1. And I want to do it via route resolves and promises, so I get the hang of promises once and for all.
Added: Here is the HTTP factory code as well as the app.run code that calls this when the app first starts. FYI, the http factory works well - the problems started when I crafted ZMDataModel - I wanted this to be a central data repository for all controllers to use -- so they did not have to call HTTP Factory each time to access data, and I could control when HTTP factory needs to be called
angular.module('zmApp.controllers').factory('ZMHttpFactory', ['$http', '$rootScope','$ionicLoading', '$ionicPopup','$timeout','ZMDataModel',
function($http, $rootScope, $ionicLoading, $ionicPopup, $timeout,ZMDataModel) {
return {
getMonitors: function() {
var monitors = [];
var apiurl = ZMDataModel.getLogin().apiurl;
var myurl = apiurl+"/monitors.json";
return $http({
url: myurl,
method: 'get'
}) //http
.then(function(response) {
var data = response.data;
//console.log("****YAY" + JSON.stringify(data));
// $rootScope.$broadcast ('handleZoneMinderMonitorsUpdate',monitors);
$ionicLoading.hide();
ZMDataModel.setMonitors(data.monitors);
ZMDataModel.setMonitorsLoaded(1);
//monitors = data.monitors;
return ZMDataModel.getMonitors();
},
function (result)
{
console.log ("**** Error in HTTP");
$ionicLoading.hide();
ZMDataModel.setMonitorsLoaded(1);
//$ionicPopup.alert ({title: "Error", template:"Error retrieving Monitors. \nPlease check if your Settings are correct. "});
return ZMDataModel.getMonitors();
}
); //then
}, //getMonitors
And here is the code in app.run that first calls this:
.run(function($ionicPlatform, $ionicPopup, $rootScope, $state,ZMDataModel, ZMHttpFactory)
{
ZMDataModel.init();
var loginData = ZMDataModel.getLogin();
if ( loginData.username && loginData.password && loginData.url && loginData.apiurl)
{
console.log ("VALID CREDENTIALS. Grabbing Monitors");
// this calls http factory getMonitors that eventually populated the ZMDataModel
// monitors array and sets monitorsLoaded to 1
ZMHttpFactory.getMonitors();
}
}
I finally solved all the problems. There were various issues with my initial attempts. My final resolved solution is here Am I returning this promise correctly?
The learnings:
a) Separating the HTTP get into a factory and the data model into another service was unnecessarily complicating life. But that separation was not the problem. Infact, the way the promise was coded above, on first run, if monitorsLoaded was 0, it would simply return the deferred promise and there was no ".success" or similar construct for me to get into the resolve code block again.
b) The biggest thing that was making me run around in loops was deferring or rejecting was simply setting a state. the return always has to be the promise - and it would return the state you set. I assumed return d.promise always means returning "in progress".

How to write services with $q and $http calls without being repetitive

I'm trying to figure out an elegant way to write AngularJS services without being so repetitive with the $q syntax.
Currently, I'm writing services like follows:
(function() {
function ServiceFactory($q, $timeout, $http) {
return {
getFoo: function() {
var deferred = $q.defer();
$timeout(function() {
$http.get('/the/foo/thing').then(function(response) {
if (response.isError) {
deferred.reject();
} else {
deferred.resolve(response.data.foo);
}
}, function() {
deferred.reject();
});
});
return deferred.promise;
}
};
}
angular.module('myapp').service('MyService', ['$q', '$timeout', '$http', ServiceFactory]);
}.call(this));
It works very well, buy I'm always writing down a bunch of code just to delay $http.get and expose a Promise. Sometimes I will have some extra stuff into success callback, like handling data, creating a different response object... But most of the time, is just like the code above: call $q.defer + $http.get().then... + return promise
So, I'm thinking about a way to clean up/reduce the code without affecting the clarity of what I'm doing, e.g if another developer open the file, it should not be a mystery.
As a side note, the $http service here is actually a decorator, handling server responses to give me a more structured object with things like response.isError and response.data.
Also, I have seen a similar solution in another question {1}, but this is not as same. Just returning the response of $http.get().then() will expose the entire response to controllers on response, and it is not the desired effect. Instead, when I call MyService.getFoo().then(...), I'm expecting a foo object as a response from service, fed by server via response.data.foo, or a call to errorCallback.
I've forgot to mention: my server is not RESTful, so $resource is not an option right now. My URLs are more like /thing/get/:id, /thing/do-stuff/:id.
{1} Similar question
After thinking for a while, I figured out a better way to write the services. I've done some changes to $http response, and now my implementation is returning a Promise whenever a Controller calls http.get() or whatever http methods. With this implementation, I've reduced the methods' code to two or three lines per request in most of the cases. Now I should try to use the AngularJS decorator setup.
A working example is here:
http://embed.plnkr.co/p4EHQAbE40XWXjBWqjIM/preview

How does the $resource `get` function work synchronously in AngularJS?

I was watching this AngularJS tutorial describing how to hook into Twitter with an Angular resource. (Video tutorial) Here is the resource that is set up in the example controller:
$scope.twitter = $resource('http://twitter.com/:action',
{action: 'search.json', q: 'angularjs', callback: 'JSON_CALLBACK'},
{get: {method: 'JSONP'}});
The tutorial shows that there are a couple ways to get data back from the resource using the get call. The first method is by passing a callback to the get function. The callback will be called with the result after the ajax request returns:
$scope.twitter.get(function(result) {
console.log('This was the result:', result);
});
I understand this method. It makes perfect sense to me. The resource represents a place on the web where you can get data, and get simply makes an ajax call to a url, gets json back, and calls the callback function with the json. The result param is that json.
It makes sense to me because it seems obvious that this is an asynchronous call. That is, under the hood, the ajax call fires, and the code following the call isn't blocked, it continues to be executed. Then at some indeterminate point later on, when the xhr is successful, the callback function is called.
Then the tutorial shows a different method that looks a lot simpler, but I don't understand how it works:
$scope.twitterResult = $scope.twitter.get();
I assume that the xhr underneath get must be asynchronous, yet in this line we are assigning the return value of the get call to a variable, as if it returned synchronously.
Am I wrong for not understanding this? How is that possible? I think it's really neat that it works, I just don't get it.
I understand that get can return something while the xhr underneath it goes off and processes asynchronously, but if you follow the code example yourself, you will see that $scope.twitterResult gets the actual twitter content before any subsequent lines are executed. For example, if you write console.log($scope.twitterResult) immediately after that line, you will see the results from twitter logged in the console, not a temporary value that is replaced later on.
More importantly, because this is possible, how can I write an Angular service that takes advantage of this same functionality? Besides ajax requests, there are other types of data stores requiring asynchronous calls that can be used in JavaScript which I would love to be able to write code for synchronously in this style. For example, IndexedDB. If I could wrap my head around how Angular's built-in resources are doing it, I would give it a shot.
$resource is not synchronous although this syntax might suggest that it is:
$scope.twitterResult = $scope.twitter.get();
What is going on here is that call to the AngularJS will, after call to twitter.get(), return immediately with the result being an empty array. Then, when the async call is finished and real data arrives from the server, the array will get updated with data. AngularJS will simply keep a reference to an array returned and will fill it in when data are available.
Here is the fragment of $resource implementation where the "magic" happens: https://github.com/angular/angular.js/blob/master/src/ngResource/resource.js#L372
This is described in the $resource documentation as well:
It is important to realize that invoking a $resource object method immediately returns an empty reference (object or array depending on isArray). Once the data is returned from the server the existing reference is populated with the actual data. This is a useful trick since usually the resource is assigned to a model which is then rendered by the view. Having an empty object results in no rendering, once the data arrives from the server then the object is populated with the data and the view automatically re-renders itself showing the new data. This means that in most case one never has to write a callback function for the action methods.
$q can do this trick too. You can convert a normal object to a 'delayed value' using something like this:
var delayedValue = function($scope, deferred, value) {
setTimeout(function() {
$scope.$apply(function () {
deferred.resolve(value);
});
}, 1000);
return deferred.promise;
};
and then use it in a controller, to get a similar effect to what $scope.twitter.get() does in the OP's example
angular.module('someApp', [])
.controller('someController', ['$scope', '$q', function($scope, $q) {
var deferred = $q.defer();
$scope.numbers = delayedValue($scope, deferred, ['some', 'numbers']);
}]);

Categories