Folks ..
I need to retrieve data from around 8 difference resources when my program begins.
These resources are independent of each other can can be called parallelly. (i.e I don't need data from one to determine what to retrieve from another resource)
However anything ahead needs to ensure that I have all the data from all the resources on-hand since they operate on each other.
So at the beginning of my controller I call a init function with the following code :
*EDIT To be more specific my exact code is as below
$scope.init = function () {
return $q.all([
Factory1.getCarData.query(), // returns a resource object like [$resolved: false, $then: function]
Factory2.getOtherData.query(), // returns a resource object like [$resolved: false, $then: function]
Factory3.getSomeOtherData.query() // returns a resource object like [$resolved: false, $then: function]
....,
resource8.query()]).then(result) {
$scope.data1 = result[1];
$scope.data2 = result1[2];...
$scope.data8 = result[3];
console.log($scope.data1); //prints as [$resolved: false, $then: function]
console.log($scope.data1[1]);
prints as undefined
doSomethingonData1($scope.data2);
doSomethingonData2($scope.data3, $scope.data4);..etc etc
}
}
Where Factory1 is defined as :
angular.module('app').factory('Factory1', function (Factory1Resource) {
var carPromise = Factory1Resource.query();
return {
getCarData: function(){ return carPromise;}
}
and Factory1Resource is defined as :
.factory('Factory1Resource', ['$resource', function($resource) {
return $resource(myURL, {}, {} );
}])
The whole point of using the factories is to ensure that the data manipulation for all 8 resources is done outside the controller in a indiviual units.
My point is .. I thought that the ".then" function would be invoked only once all resources have resolved. which means that my variables $scope.data1, $scope.data2 etc should have the actual data and not be resource objects.
This is not the case as when I do console.log($scope.data1) .. it prints as [$resolved: false, $then: function]
This breaks the flow of my program.
Now I thought I had done a lot of reading on promises and resources and I was now a enlightened person but apparently i am missing something here.
What I want is that my variables ($scope.data1, $scope.data2 etc) all contain actual data.
Any hints ? Alternately feel free to suggest any better ideas you may have as to how I should lay out my code.
then() takes a function, do it like this
.then(function(result) {
$scope.data1 = result[1];
...
});
Created a demo for you and I simplified a bit of your code. It works fine. Demo
#sza thank you so much for the advice and help here. This does seem the right way to do it however to have a fool proof system in place I did the below :
Set flags in the ".then" section of all Factory.getData.query() - so now code looks like:
Factory1.getCarData.query().then(function(){Data1Retrieved == true})
After this, I keep a watch on all flags, once all flags turn true I start rest of the processing. Again I don't know if this is the right way to do it but it sure seems to be effective.
Thank you everyone for your input again.
Instead of passing the object returned by resource.query directly to $q.all( you should pass resource.query().$promise.
According to angular $resource documentation
The Resource instances and collection have these additional
properties:
$promise: the promise of the original server interaction that created
this instance or collection.
http://docs.angularjs.org/api/ngResource.$resource
update
I decided to create a test by myself using an existing app I developed. I'm going to paste the code that worked for me. Maybe you have a different structure when you're calling the resources:
$provide.factory('GenericResource', ['$resource','$q', function($resource,$q){
var AccountResource = $resource('/user/accounts/balances'),
BankResource = $resource('/banks');
$q.all([AccountResource.query().$promise, BankResource.query().$promise]).then(function(data){
console.log(data);
});
}]);
What output of console.log is:
[
Array[3]
0: Resource
1: Resource
2: Resource
$promise: Object
$resolved: true
length: 3
__proto__: Array[0]
,
Array[4]
0: Resource
1: Resource
2: Resource
3: Resource
$promise: Object
$resolved: true
length: 4
__proto__: Array[0]
]
As you can see the the data returned contained 3 and 4 resource objects representing the data fetched. Is this result similar to the one of yours?
Related
I have a study, which has multiple cases and multiple executionMessages. Also each case has multiple executionSteps. I am able to access the study, the case and even each case's executionSteps. I cannot figure out why I cannot access the complete executionMessages. By that I meant each executionMessage has a type, message which are accessible but any objects inside executionMessage is not accessing. Here it the code
StudyService.studies.get({id: $routeParams.studyIdentifier}).$promise.then(function(study) {
$scope.study = study;
StudyService.executionMessagesForStudy.get({id: study.id}).$promise.then(function(executionMessages){
$scope.study.executionMessages = executionMessages;
});
for(var i=0;i<study.cases.length;i++){
var callback = callbackCreator(i);
StudyService.executionstepsForCase.get({id: $routeParams.studyIdentifier,caseId:study.cases[i].id})
.$promise.then(callback);
}
});
function callbackCreator(i) {
return function(executionSteps) {
$scope.study.cases[i].executionSteps = executionSteps;
}
}
It looks like you are using $resource for your service types (a guess at the .get().$promise pattern you are using), From the docs:
...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.
see: https://docs.angularjs.org/api/ngResource/service/$resource
So your code can be simplified like this:
// get() returns an object that will "fill-in" with the attributes when promise is resolved
$scope.study = StudyService.studies.get({id: $routeParams.studyIdentifier})
$scope.study.$promise.then(function(study) {
// again, executionMessages gets a placeholder that will be filled in on the resolved promise
$scope.study.executionMessages = StudyService.executionMessagesForStudy.get({id: study.id});
angular.forEach(study.cases, function(case, idx) {
// again, executionSteps is a placeholder that will be filled in on the resolved promise
var executionSteps = StudyService.executionstepsForCase.get({id: $routeParams.studyIdentifier, caseId: case.id})
// not sure if ordering is important, the service calls can return in any order
// so using push can produce list with elements in any order.
$scope.study.cases.push(executionSteps);
});
});
You still need to use the study.$promise to get the nested data after the first service call resolves.
I have this big resource object that has lots of methods inside of it with more objects, etc. I'm also converting xml into json for this.
One in particular is a resourceType object that sometimes has more than one resourceTypes. When 2 or more are present, it's an Array. When it's 1 it's just an object and I can't loop through it consistantly in my view.
I made a filter that checks if it's just an object then casts it to an array if it's not already one i'm just having trouble how to put it in my service call. Right now I have it has:
getResources: function(resourceId){
var self = this;
return Restangular.one('resource/resourceState', resourceId).get().then(function(response){
$filter('castToArray')(response.resources.resourceState.resourceTypes)
self.resources = response;
return self.resources;
});
},
This doesn't work. I want to maintain the integrity of the entire resources object but I want the resourceTypes method/object to cast to an array if only 1 is present.
In case anyone stumbles on this the fix was quite simple.
I just set the above to:
getResources: function(resourceId){
var self = this;
return Restangular.one('resource/resourceState', resourceId).get().then(function(response){
self.resources = response;
self.resources.resourceState.resourceTypes = $filter('castToArray')(response.resourceState.resourceTypes);
return self.resources;
});
},
I just needed to set the the self.resources method to the response method with the filter casted on it. Okay I'm done.
My application requires candidates to be set up with an agency and nationality (among other things). I want to set the list of agencies and nationalities in global variables so that they can be accessed by the profile screen rather than retrieved from the database every time (as they are currently). Thanks to the other questions here I've got so far... but a crucial puzzle piece is clearly missing. The data is being retrieved from the DB but then isn't visible elsewhere.
homecontroller.js which loads up the data...
//Home page Controller
function HomeCtrl($scope, $rootScope, SessionTimeoutService, GetAllAgencies, GetNationalityList){
GetAllAgencies.getData({}, function(agencieslist, $rootScope) {
SessionTimeoutService.checkIfValidLogin(agencieslist);
$rootScope.agencieslistglobal = agencieslist.data;
});
/* I tried hard-coding values here - that worked & was passed thro' ok
$rootScope.nationalitieslistglobal = [
{'nationality_id' : 0, 'name' : 'Unknown'},
{'nationality_id' : 1, 'name' : 'Known'},
{'nationality_id' : 2, 'name' : 'Pants'},
]; */
GetNationalityList.getData({}, function(nationalitieslist, $rootScope) {
SessionTimeoutService.checkIfValidLogin(nationalitieslist);
$rootScope.nationalitieslistglobal = nationalitieslist.data;
});
alert($rootScope.nationalitieslistglobal[8].name);
}
The alert doesn't fire at all.
From the candidatescontroller.js:
function CandidatesAddCtrl($scope, CandidateModel, GetNationalityList, SessionTimeoutService, $http, GetAllAgencies, $rootScope) {
/* commented out as this should now be done globally - this works when it's in place
GetAllAgencies.getData({}, function(agencieslist) {
SessionTimeoutService.checkIfValidLogin(agencieslist);
$scope.agencieslist = agencieslist.data;
});
*/
CandidateModel.getBlankCandidate();
$scope.candidateinfo = CandidateModel;
// get global agencies list
$scope.agencieslist = $rootScope.agencieslistglobal;
alert($scope.agencieslist); // shows 'undefined'
/*
GetNationalityList.getData({}, function(nationalitieslist) {
SessionTimeoutService.checkIfValidLogin(nationalitieslist);
$scope.nationalitieslist = nationalitieslist.data;
});
*/
// get global nationalities list
$scope.nationalitieslist = $rootScope.nationalitieslistglobal;
....
}
So... when I hard-code the data outside of the GetNationalityList.getData function (the bit that's commented out in the example above), it is populated & passed through OK & my drop-down list populates. When I don't do that, the $rootScope values are 'undefined'.
I have 2 theories -
somehow the homecontroller $rootScope isn't being recognised as the global I'm intending (that's just the name of the variable) and some other "passing back" action needs to be taken (I've tried several variations on "return nationalitieslist.data" and assigning it to other variables/handles). Also, nationalitieslist.data is in the same format as the hard-coded list, only it's longer.
I'm being caught out by the asynchronous nature of javascript and when I load the second page, the data just isn't there yet. I'm not convinced this is right as the DB call is finishing and I had an alert which showed me a random nationality name and it was there.
My frustration in part is coming from the fact that the GetNationalityList and GetAllAgencies code snippets work and assigns values correctly in the candidate controller (the bits that are now commented out), but are working subtly differently in the homecontroller.
Top tips, anyone, please?
I am sure this article can help.
http://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
I had somewhat of a similar issue but I needed to emit from a factory.
My problem was answered here.
Angular $rootScope.$on Undefined
Your 2nd point is more your issue, although it's not JavaScript that's async in nature, it's Angular services such as your GetAllAgencies service. You're making async calls but not waiting for the response. That's why when you assign it manually it all works. Try putting your alert() inside the callback function to the getData() call. This should highlight how the process should work.
I'm trying to post the scalar value 0 with angular resource, e.g.
myResource.save({}, 0, onSuccess, onError};
This gives a HTTP 400 error for a POST attempt with empty body.
Looking into the angular sources I see that my post data ultimately becomes the argument to the constructor of Resource, which looks like
function Resource(value){
shallowClearAndCopy(value || {}, this);
}
So it seems all falsy values will be replaced by an empty object and thus cannot be posted. Is this by design (and then: which part of the documentation did I miss?) or a bug?
The point of $resource is best illustrated when the instance methods are used instead of the class methods. Take this example from the docs:
var CreditCard = $resource('/user/:userId/card/:cardId',
{userId:123, cardId:'#id'}, {
charge: {method:'POST', params:{charge:true}}
});
...
var newCard = new CreditCard({number:'0123'});
newCard.name = "Mike Smith";
newCard.$save();
Essentially newCard is just a plain object representing a model, but enriched with some functionality. $resource provides a layer of abstraction. It's an implementation of the Active Record Pattern.
That said, if you want to post a single scalar value, then chances are that you are dealing with a service rather than a resource. In that case it's somewhat obvious that $resource (sic!) isn't the right choice.
The standard way to use the localStorage plugin for Backbone.js works like this:
App.WordList = Backbone.Collection.extend({
initialize : function(models, options){
},
localStorage : new Store('English')
}
But I want to make different, parallel wordlist collections in different languages. So, I want to be able to instantiate the name of the Store upon initialization of the collection. AFAICT, this works ok:
App.WordList = Backbone.Collection.extend({
initialize : function(models, options){
this.localStorage = new Store(options.language);
}
}
Then I can instantiate a WordList like:
english = new Wordlist([], {language: 'English'});
Or:
chinese = new Wordlist([], {language: 'Chinese'});
The thing is, I haven't really seen this done in any other examples and I'm wondering if anyone out there would have any "Eek! Don't do that, because..." sorts of reactions.
EDIT
I should add that I have already tried doing it this way:
App.WordList = Backbone.Collection.extend({
initialize : function(models, options){
},
localStorage : new Store(options.store)
}
And then:
chinese = new Wordlist([], {language: 'Chinese'});
But for some reason options.store is coming up undefined.
It's easier to explain myself as an answer, so I'll go ahead and give one.
In:
App.WordList = Backbone.Collection.extend({
initialize : function(models, options){
....
},
localStorage : new Store(options.store)
})
This is really little different from
var newInstanceConfig = {
initialize : function(models, options){
....
},
localStorage : new Store(options.store)
}
App.WordList = Backbone.Collection.extend(newInstanceConfig);
Think of it this way; there's nothing magical about the object being passed in to Backbone.Collection.extend(...). You're just passing in an ordinary object. The magic happens when Backbone.Collection.extend is invoked with that object as a parameter
Thus, the options parameter of the object method initialize is completely different that which is being passed in to new Store(...). The function being assigned initialize is defining the scope of options. Who knows where the one referred to in new Store(options.store) is defined. It could be window.options or it could be options defined in some other scope. If it's undefined, you're likely getting an error
That being said, I only see two or three strategic options (oh jeez, forgive the pun please!).
Whenever you're creating a new instance of the collection, either:
Pass in the language and let your Backbone collection create the new Store(..) where needed.
Pre-Create the Stores and either pass or give the specific Store want to that instance (either directly through its constructor or maybe you have your constructor "look-up" the appropriate pre-created Store).
And finally, I guess you could delegate the task of creating stores to another object and have it implement either options one or two. (Basically a Store Factory/Resource Manager kinda thing).
What you need to figure out is which one of those strategies should work for you. I have never used localStorage so, unfortunately, I can't help you in that regard. What I can do is ask, is there ever going to be multiple instances created from App.Wordlist where there might accidentally be created two of the same kind of Store?
In fact, I've got another question. where is this Store defined? Are you sure that's not defined somewhere in one of your other API libraries you're using? Perusing the localStorage docs I know about mentions something of a Storage constructor but nothing of a Store. So you might want to figure out that as well.
Edit #1: Nevermind, I see you mentioned where Store was defined.
I got around this by creating a method which allows you to configure the localStorage after instantiation:
var PageAssetCollection = Backbone.Collection.extend ({
initialize: <stuff>
model: <something>
...
setLocalStorage: function ( storageKey ) {
this.localStorage = new Backbone.LocalStorage(storageKey),
},
});
you can then set the localStorage after you have set up the collection:
fooPageAssets = new PageAssetCollection();
fooPageAssets.setLocalStorage('bar');