Related
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);
})
So I am still on a crash course with Angular. I am working on quite a complicated dashboard framework, all written in angular. Before I load the controllers, I need to get a bunch of dashboard settings from the server first using $HTTP. These settings are then used to control the layout of the dashboards.
So I read the way angular builds is by first running config methods, then run methods, then the controllers.
I can't use $HTTP in a config method, so I have built this in my main.js:
MetronicApp.run(['$rootScope','$http', function($rootScope,$http) {
var CUID = Cookies("CUID");
console.log('portlet settings for '+ CUID);
$http.get('/myurl/V3_portlet_settings?p_user_id='+CUID)
.then(function(response) {
console.log(response.data);
console.log('portlet status: ' + response.status);
$rootScope.$broadcast("dashSettings",response.data);
});
}]);
When I run, this all works happily and I see the data in the console.
Then in my controller:
$scope.$on( "dashSettings",
function(event,data){
$scope.dData = data;
console.log('dash data service identified in dash controller');
console.log($scope.dData.count);
} );
Couple of questions:
Is this the best way to get settings before initializing the dash. My plan would be to embed the calls that build the dash inside my $scope.$on block. I started looking at how to run a run method synchronously before the controllers initialize, but maybe I don't need to.
Any obvious idea why the $scope.$on method does not seem to fire?
Thanks in advance
A different approach would be to place your $http functions in a service or factory and then resolve these in your controller.
The key here is the use of promise. Angular documentation describes this as
A service that helps you run functions asynchronously, and use their
return values (or exceptions) when they are done processing
First create a service:
app.factory('DataService', function($http) {
var getValues= function() {
var url = '/myurl/V3_portlet_settings?p_user_id='+ CUID;
return $http.jsonp(url) // returns a promise
};
return {
getValues: getValues
}
});
And then in your controller:
myApp.controller('MyController', function ($scope, DataService) {
DataService.getValues().then( // resolve the promise using .then()
function(data){
// successcallback
// you can now safely populate the data in you controller
console.log(data);
},
function(error){
// errorcallback
console.log(error);
})
});
I think it is a better approach to use data services to handle data operations such as $http requests.
The return of promises allows for chaining of (multiple) promises and better handling of async calls.
You might find John Papa's style guide useful, especially the section about 'Separate Data Calls' (Y060) and 'Return a Promise from Data Calls' (Y061)
I have a service with a method that gets me a list of project types using a $resource. It's working well for me, except that if I make multiple nearly simultaneous calls (from say, two directives) each will create another request instead of using the same response/$promise/data.
I found this which led me to this and TL;DR, apparently it's creating a redundant $q.defer() and is actually considered to be a deferred anti-pattern.
The code below works well if the calls to get project types are significantly staggered (like more than milliseconds apart). The consecutive calls are resolved with the shared.projectTypes. It also works in the sense that if the request to get project types fails, the dfr.reject() will be triggered and be caught by .catch in the calling controller.
angular.module('projects')
.factory('projectService', function(notificationService){
// an object to share data gathered by this service
var shared = {};
// $resource for projects API
var projectResource = $resource(baseApiPath + 'projects', {}, {
...,
getProjectTypes: {
method: 'GET',
url: baseApiPath + 'projects/types'
},
...
});
// loads a list of project types
var loadProjectTypes = function(){
var dfr = $q.defer();
// if we've already done this, just return what we have.
if(shared.projectTypes){
dfr.resolve(shared.projectTypes);
}
else {
// begin anti-pattern (?)
projectResource.getProjectTypes(null,
function(response){
shared.projectTypes = response.result.projectTypes;
dfr.resolve(response);
},
function(errResponse){
console.error(errResponse);
notificationService.setNotification('error', errResponse.data.messages[0]);
dfr.reject(errResponse);
});
}
return dfr.promise;
};
return {
shared: shared,
project: projectResource,
loadProjectTypes: loadProjectTypes
};
});
So, I read that having this extra var dfr = $q.defer() is not necessary as the $resource would provide all that for me. With a bit of refactoring, I ended up with this:
...
// $resource for projects API
var projectResource = $resource(baseApiPath + 'projects', {}, {
...,
getProjectTypes: {
method: 'GET',
url: baseApiPath + 'projects/types',
isArray: true,
transformResponse: function(response){
return JSON.parse(response).result.projectTypes;
}
},
...
});
// loads a list of project types
var loadProjectTypes = function(){
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
};
...
To clarify, I have added isArray and transformResponse to the resource because my API returns a lot of extra meta information and all I wanted was an array of types. In my loadProjectTypes method, I'm including the same caching we originally had, but I'm caching the result of projectResource.getProjectTypes() instead of the actual response data (even though that might be exactly what I'm caching because of the transformResponse).
This works on the happy path (reduced calls to API, returns the same thing to everyone, etc) but my main problem is with the chaining and catching of errors.
In my original anti-pattern example, if there is an error with GET /project/types, I'm using dfr.reject() which is then passed back to my controller where I have a .catch().
This is code from the controller which actually makes the original request to get project types:
$q.all([
projectService.loadProjects(),
userService.loadUserRole('project_manager'),
userService.loadUserRole('sales_representative'),
projectService.loadProjectTypes(),
clientService.loadClients()
])
.then(function(response){
// doing stuff with response
})
.catch(function(errResponse){
// expecting errors from service to bubble through here
console.error(errResponse);
});
With the anti-pattern example, the dfr.reject is causing the error to show up here in the catch, but in my supposed non-anti-pattern example, it's not happening. I'm not sure how to reject or resolve the $resource results in the same way I was before. If one of the points of promise chaining is to have one spot to handle errors from any chain link, I was doing it right.
I tried to use $q.resolve()/reject(), since I don't have dfr anymore, but this seems dumb and doesn't work anyway.
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes(null,
function(response){
return $q.resolve(response);
},
function(errResponse){
return $q.reject(errResponse);
}));
How do I get the chain to work so that .catch() in the controller is where the errors get handled?
Did I actually implement the anti-pattern in my original code, or was that one of the accepted ways to use $q.defer() and it wasn't an anti-pattern at all?
In the second link I posted, there is an answer that says:
"What's wrong with it? But the pattern works! Lucky you.
Unfortunately, it probably doesn't, as you likely forgot some edge
case. In more than half of the occurrences I've seen, the author has
forgotten to take care of the error handler."
However, my original code was addressing the errors. It was working, except that each caller was getting it's own promise. I feel that's where I missed something.
I might be confused, but I'm thinking that the loadProjectTypes method should return the same promise/data to anyone who calls it, no matter when it's called. It should be the one true source of anything projectTypes and only make the call once, the very first time.
Any time I look for any of this (lots of purple/visited google links on these subjects), everyone is either showing chaining with contrived examples, or only using $http, or something else. I haven't found anyone doing error catching in a promise chain that uses $resource.
UPDATE: Adding my requirements for the solution. I posted them in my answer, but wanted to include them in the original post too.
Requirement 1: Allows multiple calls to the method, but only makes one API request which updates all callers with the same data.
Requirement 2: Must be able to use result of method as actual data, just as the promise spec intends. var myStuff = service.loadStuff() should actually set myStuff to be "stuff".
Requirement 3: Must allow promise chaining so that all errors in any part of the chain can be caught by a single catch at the end of the chain. As I've found in my solution, there can be more than one chain, and more than one catch, but the point is that each chain has a catch, and any "links" in the chain that break should all report their errors to their respective catch.
Isn't that always the way, as soon as you speak your problems, you come across your solution.
Requirement 1: Only make one request per method call. This is solved with the original fix to the anti-pattern. This will either always return the $resource result by either returning the cached $resource or returning and caching at the same time.
var loadProjectTypes = function(){
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
};
Requirement 2: Be able to use the service method as a promise where I can set the value of a $scope variable directly to the result of loadProjectTypes(). Using the revised method above, I can simply state $scope.theTypes = projectService.loadProjectTypes() and it'll automatically be filled with the list of types when they come in, just as the promise spec intends.
Requirement 3: Be able to chain together multiple $resource calls and have their errors be caught by a single .catch(). By using the $promise of the result of loadProjectTypes within $q.all(), I can catch any errors in any catch I want.
$q.all([
...,
projectService.loadProjectTypes().$promise,
...
])
.then(function(response){
// my project types comes in as response[n]
})
.catch(function(errResponse){
// but any errors will be caught here
});
Technically, I can put catches in different places and they'll all work the same. Anytime I have loadProjectTypes(), I can use a .catch() and my errors will be handled there. Each loader of types can handle the API being down in it's own way. This could be really good actually. A controller might get the UI to display a message and a small directive might just display something else, or nothing at all. They each can handle the bad in their own way.
My service, directive and controller look like this now:
angular.module('projects')
.factory('projectService', function(notificationService){
// an object to share data gathered by this service
var shared = {};
// $resource for projects API
var projectResource = $resource(baseApiPath + 'projects', {}, {
...,
getProjectTypes: {
method: 'GET',
url: baseApiPath + 'projects/types',
isArray: true,
transformResponse: function(response){
return JSON.parse(response).result.projectTypes;
}
},
...
});
// loads a list of project types
var loadProjectTypes = function(){
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
};
return {
shared: shared,
project: projectResource,
loadProjectTypes: loadProjectTypes
};
});
angular.module('projects')
.directive('projectPageHeader', ['projectService', function(projectService){
return {
restrict: 'E',
scope: {
active: '#',
},
templateUrl: 'src/js/apps/projects/partials/dir_projectPageHeader.html',
replace: true,
controller: function($scope){
$scope.projectService = projectService;
// sets the types to the array of types
// as given by the transformResponse
$scope.types = projectService.getProjectTypes();
// could also do a .$promise.catch here if I wanted.
// all catches will fire if get projectTypes fails.
}
};
}]);
angular.module('projects')
.controller('projectListPageController', [
'$scope','projectService',
function($scope, projectService){
// load it all up
$q.all([
projectService.loadProjectDetails($routeParams.projectId).$promise,
userService.loadUserRole('project_manager').$promise,
userService.loadUserRole('sales_representative').$promise,
projectService.loadProjectStatuses().$promise,
projectService.loadProjectTypes().$promise,
clientService.loadClients().$promise
])
.then(function(response){
// do work with any/all the responses
})
.catch(function(errResponse){
// catches any errors from any of the $promises above.
})
}]);
Since the loadProjectTypes (or any other load_____ method) saves the types within the service it comes from, I don't really need to do any storing on the controller. projectService.shared.projectTypes is universal across the entire app. The .then() method in my controller could potentially be noop if all the services were storing the results of their loads internally (which is how I like it) unless there was some view specific thing I needed to do with them. I typically only use controllers for entire pages, or $modals. Everything else is broken up into directives and most information and logic is in services.
I'm leaving the question open in case someone has a better solution. I like the one that Jack A. posted, but I feel it makes my load___ methods more verbose than they already are. Since there are a few of them with slight differences, it leads to a lot of redundant code, or complex 'smart' methods in my actual code. It definitely solves Requirement 1 and possibly 2 and 3 though.
UPDATE (GOTCHA):
So, I've been using this pattern for a few days now and it's working really exactly as I intend. It's really streamlined our process; however, I recently came upon a gotcha when using a method like loadProjectTypes in a singular context (i.e.: outside of $q.all()).
If you just use the load method like so:
// This code is just placed in your controllers init section
loadProjectTypes()
.$promise
.then(function(response){
// ... do something with response (or noop)
})
.catch(function(errResponse){
// ... do something with error
});
You will run into a situation when that controller 'refreshes'. For example, you have the code above in controllerA, you change "pages" which uses controllerB, then you go back to the first "page" and controllerA refreshes and tries to run this again. The error you get is that "there is no .then of undefined."
Inspecting this in the console, the first time loadProjectTypes() runs, it returns the response from the $resource (which includes $promise AND all the projectType data). The second time - coming back from controllerB - it will only hold the projectType data. There is no more $promise because you are not returning the result of a $resource, you returned the cached shared.projectTypes that you set after the first time. That's why we did all this, remember? I'm not sure why this goes away since that's what you saved to shared.projectTypes, but it does, and it doesn't actually matter.
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
For me, the easiest fix was to just have loadProjectTypes().$promise as the only member of a $q.all() set:
// again, this code is just placed somewhere near the top of your controller
$q.all([
loadProjectTypes().$promise
])
.then(...)
.catch(...);
In most cases, my controllers will be getting more than one thing so this would've happened eventually, but there will always be a situation where you only need to load one thing. Using a single item set in $q.all() is the only way to have no issues when using this solution. It's not that bad really, could've been worse.
I wrote something very similar to this a while ago, with a couple key differences:
I only create the promise when the data is already in the cache and return the native promise when an actual request is initiated.
I added a third state for when a request for the resource is already pending.
A simplified version of the code looks like this:
module.factory("templateService", function ($templateCache, $q, $http) {
var requests = {};
return {
getTemplate: function getTemplate(key, url) {
var data = $templateCache.get(key);
// if data already in cache, create a promise to deliver the data
if (data) {
var deferred = $q.defer();
var promise = deferred.promise;
deferred.resolve({ data: data });
return promise;
}
// else if there is an open request for the resource, return the existing promise
else if (requests[url]) {
return requests[url];
}
// else initiate a new request
else {
var req = $http.get(url);
requests[url] = req;
req.success(function (data) {
delete requests[url];
$templateCache.put(key, data);
});
return req;
}
},
};
});
I have a real problem with JavaScript promises that I've been trying to solve for the last few hours and I just can't seem to fix it. My experience with promises is limited so I'm open to the idea that my approach is simply incorrect.
Right now I'm building an app that requires a two-step process:
Connect to an external PaaS service, which returns a promise
Within that promise, retrieve some data
Here's a sample of a factory I created:
app.factory('serviceFactory', [
function() {
var getData = function getData() {
service.connect(apiKey).then(function() {
service.getData('dataStore').then(function(result) {
// Retrieve data
return result;
}, errorFunction);
},
errorFunction);
};
return {
getData: getData
};
}
]);
As you can see, there are nested promises here. What's causing me problems is when I try to use the data from the most deeply-nested promise within an AngularJS view. Specifically, I want to use the data from that promise in an ng-repeat statement. But no matter what I try, it just won't show up. I've attempted to assign data within the promise instead of returning, like so:
service.getData('dataStore').then(function(result) {
// Retrieve data
// Assigned the enclosing scope's this to 'self'
self.data = result;
}, errorFunction);
That doesn't work either. I've tried a variety of other approaches, but I just can't seem to get that data to the view. There's no problem getting it to show up in a console.log(data)call, so I know the data is coming back correctly. Does anyone have experience solving a problem like this?
I would suggest that you'll try to avoid nested promises. You can take a look at this blog post, which will let you see how you can avoid 'promise soup' and have promise chaining instead.
As for your question, I would recommend the following:
A quick solution will be to fix your problem. You are returning the factory method wrong:
app.factory('serviceFactory', [
function() {
var getData = function getData() {
return service.connect(apiKey).then(function() {
service.getData('dataStore').then(function(result) {
// Retrieve data
return result;
}, errorFunction);
},
errorFunction);
};//here you should close the 'getData method
return {
getData: getData
};
}
]);
But, you can refactor your code to chain your promises. Something like:
app.factory('serviceFactory', [
function() {
var connect = function connect() {
return service.connect(apiKey);
};
var getData = function getData(data) {
return service.getData(data);
};
return {
getData: getData,
connect: connect
};
}
]);
Now, you can do something like this:
serviceFactory.connect(apiKey)
.then(serviceFactory.getData)
.then(function(result){
//use data here
})
All of this should be tested - you can add a plunker or jsbin if you want a working solution...
EDIT
I think that you have another problem here. You are mixing between serviceFactory and service. I'm not sure that I understand if this is the same service, or which is who. Can you provide a more detailed code or add plunker/jsbin etc.
I've edited this answer, which I originally deleted because I didn't explain what I meant very clearly and it garnered some downvotes (without explanation, but that's my guess). Anyway, here is a more complete answer.
I suspect that your problem is that whatever PaaS you are using has no awareness of Angular, and Angular likewise has no awareness of the PaaS. You say in your question that the PaaS has methods that return promises, but if Angular is not aware of those promises, then, when the promises resolve, Angular does not know to update the DOM. Angular does this via the digest cycle which is where Angular checks everything that it is watching to see if it has changed. When using $q (or other Angular services like $http), Angular knows to automatically kick off a digest cycle when they resolve. It does not, however, kick off a digest cycle when promises created by other means resolve.
This is what I think is happening in your code. Your PaaS is giving you promises, which are resolving properly (you said you can see the results via console), but your HTML is not being updated.
I modified the plunkr we were working on to demonstrate this in action. I created a mock PaaS (not knowing what you are using) that creates promises using jQuery and resolves them. As you can see, when the promises resolve, the result is logged to the console, but the DOM is not resolved.
angular.module("app",[])
.value("mockPaaS", mockPaaS)
.factory("dataFactory", function($q, mockPaaS){
function getData(){
return mockPaaS.connect()
.then(mockPaaS.getData);
}
return {
getData: getData
}
})
.controller("DataController", function (dataFactory) {
var vm = this;
dataFactory.getData().then(function(result){
console.log(result);
vm.dataArr = result;
});
})
.directive("myApp", function(){
return {
bindToController: true,
controller: "DataController",
controllerAs: 'myApp',
template: "<div ng-repeat='i in myApp.dataArr'>{{i}}</div>"
};
});
I was originally suggesting that you could solve this problem by adding a $scope.$apply() after you capture the result of the promise. I've forked the Plunker and you can see here it does, in fact update the DOM.
.controller("DataController", function ($scope, dataFactory) {
var vm = this;
dataFactory.getData().then(function(result){
console.log(result);
vm.dataArr = result;
$scope.$apply();
});
})
There is, however, a more idiomatic solution. When you get a promise from outside angular that you need to use in Angular, you can wrap that promise using $q.when (an Angular aware promise), and when the external promise resolves, Angular should kick off it's digest cycle naturally.
.factory("dataFactory", function($q, mockPaaS){
function getData(){
return $q.when(mockPaaS.connect()
.then(mockPaaS.getData));
}
return {
getData: getData
}
})
.controller("DataController", function (dataFactory) {
var vm = this;
dataFactory.getData().then(function(result){
console.log(result);
vm.dataArr = result;
});
})
Ben Nadel gives a nice explanation of this issue here.
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".