I believe this has to do with the way JS Closures work, but I am not totally sure. I am using an AngularJS service to manage the life-cycle of a model that is used within my application. The service uses a combination of fetch() and save() to run GET and POST requests get and update the model from an API. After I fetch() the object, I attempt to place the result into an object sitting in the service where it can be fetched later on. My problem, is that after a successful save(), I take the result and place it into the same object to essentially "update" the object that is on the client with the correct object that is on the server (hence the result of the POST is just an echo of the payload if all is successful).
The problem is that my object is not persisting, and all subsequent calls to save() contain a "stale" object that is not completely updated.
Here is my service:
app.factory('MailboxSubscription', function (API, $q, $stateParams, $rootScope) {
var Subscription = null; //THIS IS MODEL THAT I TRY TO UPDATE CONSTANTLY
var isBusy = false;
var service = {};
var deferred;
var defaultFailure = function(res){
}
service.fetch = function (success, force, failure) {
if(!failure){ failure = defaultFailure;}
if(isBusy){
deferred.promise.then(success, failure);
return deferred.promise;
}else{
deferred = $q.defer();
}
if(Subscription && !force){ // ONCE THE MODEL HAS BEEN FETCHED ONCE, IT STAYS IN MEMORY AND ALL SUBSEQUENT CALLS WILL SKIP THE API CALL AND JUST RETURN THIS OBJECT
deferred.resolve(Subscription);
}else{
//Make the API call to get the data
//Make the API call to get the data
if(typeof(success) === 'function'){
var ServiceId = $stateParams.serviceId;
}else{
var ServiceId = success;
}
isBusy = true;
API.Backups.O365.Exchange.get({id : ServiceId || $stateParams.serviceId}, function(res){
isBusy = false;
if(res.success){
Subscription = res.result; // ON A FIRST-TIME FETCH, THIS API CALL IS USED TO GET THE MODEL
deferred.resolve(Subscription);
}else{
deferred.reject(res);
}
}, function(res){
isBusy = false;
deferred.reject(res);
});
}
deferred.promise.then(success, failure);
return deferred.promise;
}
service.save = function(success, failure){
if(!failure){ failure = function(){};}
if(!success){ success = function(){};}
var deferred = $q.defer();
API.Backups.O365.Exchange.update({id :$rootScope.ServiceId || $stateParams.serviceId}, Subscription, function(res){
if(res.success){
Subscription = res.result; // AFTER AN UPDATE IS MADE AND THE OBJECT IS SAVED, I TRY TO SET THE RESULT TO Subscription.
deferred.resolve(res);
}else{
deferred.reject(res);
}
}, function(res){
deferred.reject(res);
});
deferred.promise.then(success, failure);
return deferred.promise;
}
service.get = function(){
return Subscription;
}
return service;
});
So the problem appears to stem from trying to use Subscription as a centralized resource for storing the model, but the model is not updating correctly.
If you are looking to have that Subscription model updated throughout the service, I'd suggested when you call MailboxSubscription.fetch() and MailboxSubscription.save()in your controller, you use MailboxSubscription.get() in the .then() method of your calls.
// get initial value of Subscription model
$scope.Subscription = MailboxSubscription.get();
// let's fetch
MailboxSubscription.fetch().then(
// callback
function() {
// let's get the updated model
$scope.Subscription = MailboxSubscription.get();
},
// errback
function() {
// handle error
}
);
// let's save
MailboxSubscription.save().then(
// callback
function() {
// let's get the updated model
$scope.Subscription = MailboxSubscription.get();
},
// errback
function() {
// handle error
}
);
Also, I've created a working jsfiddle simplifying your use case. It works fine. Maybe there is something that can be gleamed from that (I am using $timeout to spoof your API calls).
Related
I have an API call that's working great, but I'd like to use it on several controllers so I moved it to it's own service. I'm running into what looks like a classic Scope issue or a misunderstanding of Angular's digest cycle.
'use strict';
myApp.factory('Stuff',['$http', function ($http) {
var Stuff = {};
Stuff.data = {};
Stuff.api = 'http://localhost:8080/api/';
Stuff.getStuff = function() {
var http_stuff_config = {
method: 'GET',
url: Stuff.api + 'stuff/'
};
$http(http_stuff_config).then(function successCallback(response) {
Stuff.data = (response.data);
console.log(Stuff.data); // Returns populated object.
},function errorCallback(response) {
console.log(response.statusText);
});
};
Stuff.getStuff();
console.log(Stuff.data); // Returns empty object.
return Stuff;
}]);
myApp.controller('appController', ['$scope','Stuff',function($scope,Stuff) {
$scope.stuff = Stuff;
console.log($scope.stuff.data); // Returns empty object.
$scope.stuff.getJobs();
console.log($scope.stuff.data); // Returns empty object.
}]);
Here's the big clue. The essential output of above, in order is...
empty object (in service after calling method)
empty object (in controller before calling method)
empty object (in controller after calling method)
populated object (in method execution from service)
populated object (in method execution from controller)
So somewhere between the scope of the getStuff() method and Angular's order of operations, I'm doing something remarkably foolish. Thank you in advance.
You need to add returns on your service, or else the promise will not be returned to the controller. It is not good practice to just store the returns in your services AND NOT return the result to the controller.
This is considered bad practice because, any time you update the data on the service everyone will need to apply $scope.$watch to the service to look for updates. This can be very expensive in large scale apps.
The best Idea is to return the data to the calling controller (if you do not need to cache it, this we can talk about later) and let the controller access it via the promise service.getthing().then(function(result){});
myApp.factory('Stuff',['$http', function ($http) {
var Stuff = {};
Stuff.data = {};
Stuff.api = 'http://localhost:8080/api/';
Stuff.getStuff = function() {
var http_stuff_config = {
method: 'GET',
url: Stuff.api + 'stuff/'
};
return $http(http_stuff_config).then(function successCallback(response) {
return response.data;
console.log(Stuff.data); // Returns populated object.
},function errorCallback(response) {
console.log(response.statusText);
});
};
Stuff.getStuff();
console.log(Stuff.data); // Returns empty object.
return Stuff;
}]);
myApp.controller('appController', ['$scope','Stuff',function($scope,Stuff) {
$scope.stuff = Stuff;
console.log($scope.stuff.data); // Returns empty object.
$scope.stuff.getJobs().then(function(result) {$scope.stuff = result; console.log(result);});
console.log($scope.stuff.data); // Returns empty object.
}]);
I recommend you not to store the result inside the service itself (Stuff.data). Just return your data in the getStuff function and let the appController's scope store the data instead.
remember that $scope.stuff.getJobs() is async
(meaning you can't actually call console.log($scope.stuff.data) on the next line and get the data)
Now if you had a view, with something like <span ng-bind="stuff.data.property"> you could see it work just fine because the view will update by itself when the async function is done. (this is part of angular)
You need to understand that when you run $http, it is making an AJAX request. therefore it will not return an result immediately.
Therefore, if you attempt to use the data coming from $scope.stuff.getJobs(); immediate after invoking this function, you are likely to get nothing.
What you should do is to have your Stuff.getJobs() return a promise, and use promise.then(your own success handler) to correctly handle the returned response.
I have cleaned up your code a little bit. The following is a running sample of your code retrieving data from Yahoo Weather API.
You can play with it on CODEPEN.
html:
<div ng-app="myApp" ng-controller="appController">
<p>{{data}}</p>
</div>
JS:
var myApp = angular.module("myApp", []);
myApp.factory('Stuff',['$http', function ($http) {
var Stuff = {};
Stuff.data = {};
//sample yahoo weather api
Stuff.api = 'https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22nome%2C%20ak%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys';
Stuff.getData = function() {
var http_stuff_config = {
method: 'GET',
url: Stuff.api + 'stuff/'
};
return $http(http_stuff_config);
};
return Stuff;
}]);
myApp.controller('appController', ['$scope','Stuff',function($scope,Stuff) {
$scope.data = "$http service not ran";
var uncompletedAjaxCall = Stuff.getData();
uncompletedAjaxCall.then(
function(responseData){
$scope.data = responseData;
},
function(errorMsg){}
);
}]);
Please find below the angularjs factory method to call http request:
var HttpService = angular.module("HttpService",[]);
HttpService.factory("HttpServiceFactory",['$http', '$q', '$location', '$rootScope' ,function($http, $q, $location, $rootScope){
return {
getData: function(url, headers, bOnErrorRedirect, bShowInPageError, params){
var headerParam = {'Accept':'application/json'};
if(headers !== undefined || headers !== null){
headerParam = $.extend(headerParam, headers);
}
var updatedParams = {'TimeStamp':new Date().getTime()};
updatedParams = $.extend(params, updatedParams);
var deferred = $q.defer();
$http.get(url,{
headers: headerParam,
params : updatedParams
}).success(function(successResponse){
if(successResponse){
var responseJSON = angular.fromJson(successResponse);
if(responseJSON && responseJSON.messages && responseJSON.messages.length){
//Process Error
}else{
deferred.resolve(successResponse);
}
}else{
deferred.resolve(successResponse);
}
}).error(function(errorResponse , status){
//Process Error
console.error("status here:: "+status);
});
return deferred.promise;
}
}
}]);
And I am calling this method in controller with all required dependencies as below:
HttpServiceFactory.getData(args.sURL,null,false,true,args.oQueryParams).then(function(response){
scope.bDataLoading = false;
// process data
})
.catch(function(oResponse) {
scope.bDataLoading = false;
scope.bDisplayError = true;
// process error
});
Here everything works fine. But the issue is when I've multiple http calls on a page, the UI freezes and does not allow to interact till the request has been processed.
For example, on a page I am displaying 2 angular-ui-grid based on user's selected criteria by input box and calendar control. In such case, the UI freezes until both grids have been displayed or error message has been displayed.
During http service call, user can not do anything but simply wait to finish the request.
How do I resolve the issue of UI freezing ? Is it a true async behavior ? If not, what am I missing to achieve correct async behavior ?
I have an factory that gets data from my backend:
as.factory("abbdata", function GetAbbData($http,$rootScope,$routeParams,$q) { //$q = promise
var deffered = $q.defer();
var data = [];
var abbdata = {};
abbdata.async = function () {
$http.get($rootScope.appUrl + '/nao/summary/' + $routeParams['id']).success(function(d) {
data = d.abbData;
deffered.resolve();
});
return deffered.promise;
};
abbdata.data = function() {
return data;
};
return abbdata;
});
A call my factory like this in my controller:
abbdata.async().then(function() {
$scope.abbData = abbdata.data(); //Contains data
});
When I do a console.log($scope.abbData) outside my service call, just underneath, the result Is undifined. Why? Should not the $scope.abbData contain the data from my service after I call it?
EDIT:
You need to pass the data that should be returned into the resolve function like this:
deffered.resolve(data);
EDIT:
To get the data in the controller do this:
abbdata.async().then(function(data) {
$scope.abbData = data; //Contains data
});
Why don't you simply return that value from the async call in the first place?
You can chain promises so by attaching a success handler in your factory and returning a value from that you can simplify your code to:
as.factory("abbdata", function GetAbbData($http,$rootScope,$routeParams) {
return {
async: function () {
return $http.get($rootScope.appUrl + '/nao/summary/' + $routeParams['id']).success(function(d) {
return d.data.abbData;
});
}
}
});
And then use it like
abbdata.async().then(function(data) {
$scope.abbData = data; //Contains data
});
if you console.log($scope.abbData) outside the service call it should show undefined, since the call is asynchronous.
abbdata.async().then(function() {
$scope.abbData = abbdata.data(); //Contains data
});
console.log($scope.abbData) // this should show undefined
The console.log($scope.abbData) just after setting the abbData should show the data
abbdata.async().then(function() {
$scope.abbData = abbdata.data(); //Contains data
console.log($scope.abbData) // this should show the data
});
EDIT
you can use abbData from your service call like for example
angular.module('myApp', []).controller('HomeCtrl', function($scope, abbdata){
var updateUI;
$scope.abbData = [];
abbdata.async().then(function() {
$scope.abbData = abbdata.data(); //Contains data
updateUI();
});
updateUI = function(){
//do something with $scope.abbData
}
});
EDIT 2
On response to your query, I would do something like,
angular.module('myApp', [])
.controller('JobsCtrl', function($scope, $jobService) {
$scope.jobs = [];
$jobService.all().then(function(jobs) {
$scope.jobs = jobs;
});
})
.service('$jobService', function ($q, $http) {
return {
all: function () {
var deferred = $q.defer();
$http({
url: 'http://url',
method: "GET"
}).success(function (data) {
deferred.resolve(data);
}).error(function () {
deferred.reject("connection issue");
});
return deferred.promise;
}
}
});
associated view
<body ng-app = "myApp">
<div ng-controller = "JobsCtrl">
<div ng-repeat="job in jobs track by job.id">
<a href="#/tab/jobs/{{job.id}}" class="item item-icon-right">
<h2>{{job.job_name}}</h2>
<p>DUE DATE: {{job.job_due_date}}</p>
</a>
</div>
<div>
</body>
Here the service an all function which returns a promise, i.e. it will notify when data is fetched.
in the controller the service is called and as soon the service call is resolved the $scope.jobs is assigned by the resolved data.
the $scope.jobs is used in the angular view. as soon as the jobs data are resolved, i.e. $scope.jobs is assigned, the view is updated.
hope this helps
I had a quick look, I have 2 ideas:
First theory: your service is returning undefined.
Second theory: you need to run $scope.$apply();
See this fiddler: https://jsfiddle.net/Lgfxtfm2/1/
'use strict';
var GetAbbData = function($q) {
//$q = promise
var deffered = $q.defer();
var data = [];
var abbdata = {};
abbdata.async = function () {
setTimeout(function() {
//1: set dummy data
//data = [200, 201];
//2: do nothing
//
//3: set data as undefined
//data = undefined;
deffered.resolve();
}, 100);
return deffered.promise;
};
abbdata.data = function() {
return data;
};
return abbdata;
};
var abbdata = GetAbbData(Q)
abbdata.async().then(function() {
console.log(abbdata.data()); //Contains data
});
I have stripped away a lot of dependencies and replaced $q with Q just for my own ease.
In the above example, I first attempted to run the code with dummy data, the console output the expected data, then I tried to not assign the data, and I get an empty array. This is why I assume that if you are seeing 'undefined' you must be explicitly setting the value to 'undefined'.
That aside, I also noticed that you were testing the result by reading directly from $scope. I know that when not inside the angular scope, doing operations on the $scope object does not necessarily happen in a timely manner, and typing $scope.$apply() usually fixes this. Usually, when using $http, angular keeps you in the appropriate scope, but you are creating your own promise using $q so this could be another potential issue.
Finally, the other two answers have pointed out that you are not using promises in the standard way. Although your code works fine, it is not normal to set your data directly onto your service and retrieve it from there. You can keep your service stateless by simply resolving your promise with the data that you want to process in the then method as shown by the answers by Anzeo and Markus.
I hope I was able to find the solution, good luck.
Dipun
as.factory("abbdata", function GetAbbData($http,$rootScope,$routeParams,$q) { //$q = promise
var deffered = $q.defer();
var data = [];
var abbdata = {};
abbdata.async = function () {
$http.get($rootScope.appUrl + '/nao/summary/' + $routeParams['id']).success(function(d) {
data = d.abbData;
deffered.resolve(data);
});
return deffered.promise;
};
abbdata.data = function() {
return data;
};
return abbdata;
});
I can't see what the problem with this is.
I'm trying to fetch data on a different server, the url within the collection is correct but returns a 404 error. When trying to fetch the data the error function is triggered and no data is returned. The php script that returns the data works and gives me the output as expected. Can anyone see what's wrong with my code?
Thanks in advance :)
// function within view to fetch data
fetchData: function()
{
console.log('fetchData')
// Assign scope.
var $this = this;
// Set the colletion.
this.collection = new BookmarkCollection();
console.log(this.collection)
// Call server to get data.
this.collection.fetch(
{
cache: false,
success: function(collection, response)
{
console.log(collection)
// If there are no errors.
if (!collection.errors)
{
// Set JSON of collection to global variable.
app.userBookmarks = collection.toJSON();
// $this.loaded=true;
// Call function to render view.
$this.render();
}
// END if.
},
error: function(collection, response)
{
console.log('fetchData error')
console.log(collection)
console.log(response)
}
});
},
// end of function
Model and collection:
BookmarkModel = Backbone.Model.extend(
{
idAttribute: 'lineNavRef'
});
BookmarkCollection = Backbone.Collection.extend(
{
model: BookmarkModel,
//urlRoot: 'data/getBookmarks.php',
urlRoot: 'http://' + app.Domain + ':' + app.serverPort + '/data/getBookmarks.php?fromCrm=true',
url: function()
{
console.log(this.urlRoot)
return this.urlRoot;
},
parse: function (data, xhr)
{
console.log(data)
// Default error status.
this.errors = false;
if (data.responseCode < 1 || data.errorCode < 1)
{
this.errors = true;
}
return data;
}
});
You can make the requests using JSONP (read about here: http://en.wikipedia.org/wiki/JSONP).
To achive it using Backbone, simply do this:
var collection = new MyCollection();
collection.fetch({ dataType: 'jsonp' });
You backend must ready to do this. The server will receive a callback name generated by jQuery, passed on the query string. So the server must respond:
name_of_callback_fuction_generated({ YOUR DATA HERE });
Hope I've helped.
This is a cross domain request - no can do. Will need to use a local script and use curl to access the one on the other domain.
My problem is that I don't know where to store data I need to access in the final callbacks for a http request. In jQuery I would just do the following
var token = $.get('/some-url', {}, someCallback);
token.oSomeObject = {data: 'some data'};
function someCallback( data, status, token ){
token.oSomeObject.data // 'some data'
}
I use the token to store request specific data.
Now the only way I find to acheive the this in Angular is to store the data in the actual config:
var config = {
url: '/some-url',
method: 'GET',
oSomeObject: { data: 'some data' }
};
$http(config).success(someCallback);
function someCallback( data, status, headers, config ){
config.oSomeObject.data // 'some data'
}
For one this prevents you from using the short-hand calls ($http.get, $http.post) I also find it's much a much more obtrusive way when wrapping the calls in a specific service module.
Is there any other way of doing this?
Updated to clarify
I'm probably just missing something simple here not understanding how to properly use the promise API, but just to be sure we're on the same page let me give you a bit more detail to the issue.
I have 2 files: 1) Controller.js 2) AjaxServices.js (all ajax calls are defined here as methods on a service).
AjaxServices.js looks like this:
app.service('AjaxService', function( $http ){
var self = this;
this.createUser = function( oUser, fSuccessCallback ){
return $http.put('/api/user', {oUser: oUser})
.success(fSuccessCallback);
}
}
Controller.js looks like this:
app.controller('MyController', function( $scope, AjaxServices ){
$scope.saveUser = function( oUser ){
var oPromise = AjaxServices.createUser( oUser, $scope.onUserSaved );
oPromise.oUser = oUser // this is how I solve it in jQuery.ajax. The oPromise
// is then sent as the token object in the onUserSaved
// callback
}
$scope.onUserSaved = function( oStatus, status, headers, config ){
oStatus.id // here is the id of the newly created user
// which I want to now hook on to the oUser local object
}
}
How would you achieve the same thing using the promise API?
UPDATE 2. Some notes on your updated code:
You don't have to pass the callback to the service. This effectivelly kills the purpose of the promise. Let the service to it's job without coupling it to any consumer of it's data.
app.service('AjaxService', function( $http ){
var self = this;
this.createUser = function( oUser ){
return $http.put('/api/user', {oUser: oUser});
}
}
Now the service does't care about the the callbacks, meaning you could attach multiple callbacks to same service $http call. Ok, let's move on to the service data consumer, in this case it's the controller:
app.controller('MyController', function( $scope, AjaxServices ){
$scope.saveUser = function( oUser ){
var oOriginalPromise = AjaxServices.createUser( oUser );
//lets modify the orginal promise to include our oUser
var oModifiedPromise = oOriginalPromise.then(function(response) {
response.oUser = oUser;
return response;
});
//in the code above we've chained original promise with .then,
//modified response object and returned it for next promise to receive
//at the same time, .then created a new promise
//finally, after modifying our response, we can pass it to the desired callback.
//note, that .success and .error are $http specific promise methods,
//in all other cases use .then(fnSuccess, fnError)
oModifiedPromise.then($scope.onUserSaved);
}
$scope.onUserSaved = function(responseWithUser) {
responseWithUser.oUser;
}
}
Of course, it is still a bit awkward, since the oUser could be saved in the controller's scope and could be accessed directly from there in $scope.onUserSaved.
UPDATE. I will clarify my answer. You can chain promises in any scope, anywhere. Here's another example with token being injected by service:
myModule.factory('Service', function($http) {
return new function() {
this.getData = function() {
return $http.get(url).then(function(response) {
response.token = myToken;
return response;
}
}
}
});
You could even extend or wrap $http service and inject tokens on responses without your services knowing about it.
If you do this with all your requests, maybe a $httpInterceptor would be more appropriate. Read more about intercepting http calls here
Original answer
Since $http provides you with promise, you can chain it to another promise which would inject token in response:
myModule.factory('Service', function($http) {
return new function() {
this.getData = function() {
return $http.get(url);
}
}
});
//in controller or other service:
var myToken;
var tokenizedPromise = Service.getData().then(function(response) {
response.token = myToken;
return response;
});
The final consumer of the promise has access to the token as well