In the success callback to an ajax query, I have to
a) update the property of an object in an array (and I have to locate that object in the array)
b) save that array again in storage
c) broadcast an event
I'm (trying to) accomplishing that by doing this (code below)
updateTheArray(myarray,'date', $scope.date);
myStorage.put(myarray);
$scope.$broadcast('alldone');
As I'm concerned these are happening out of order or could happen out of order,I would like to be able to do something like (in pseudo code) in the success callback
var updated = updateTheArray(); //update a property on an object in array
updated.promise().then(function(){
myStorage.put(array); //saves
}).done(function(){
$scope.$broadcast('alldone'); //broadcast
}
However, the way I've written the storage and the updateTheArray function are not setup to do that.
Is there a way in Angular to make sure the next function only runs once the first one is complete?
Leaving aside the updateTheArray function, even if I try to use the storage function in promise like this
var save = myStorage.put(array);
save.promise().done(function(){
console.log('blah');
});
I get a cannot read property promise of undefined error, so how do I make that storage return a promise object?
updateTheArray (finds a particular object in an array and changes a property)
function updateTheArray(array, attr, value){
var len = array.length;
len = len -1;
for (var i = len; i > 0; i --){
if(array[i][attr] === value) { //find by date
array[i]['completed'] = true; //set some property to true
}
}
}
storage
app.factory('myStorage', function(){
return {
get: function(){
return JSON.parse(localStorage.getItem(STORAGE_ID) || '[]');
},
put: function(myarray){
localStorage.setItem(STORAGE_ID, JSON.stringify(myarray));
}
}
}
As described in this answer, local storage calls are synchronous, so there is no need to wait for myStorage.put to complete.
The reason for the cannot read property promise of undefined error is that you aren't returning anything at all from your put function on myStorage (but, as above, you don't need to return a promise anyway as it's a synchronous call).
The updateTheArray implementation you have shown is also synchronous, so the whole thing should work in order as-is, without any callbacks or promises required.
Related
In my app.run I have an function which get some text from an global variable from an other file. In that variable there are some keywords with an language attribute, which allows me to only get the requested text and filter out the right language.
I made an function for that but I can't get access the $rootScope.language variable in that function which I need for getting the right text.
function getTourTranslation(key){
console.log("getTourTranslation", key);
var lang = $rootScope.language;
var step = _.get(steps, key);
return step[lang]
}
So language is undefined, and so $rootScope.language is, but the variable exists and contains the right language key for the current language on the site. How can I get the content of that variabele without passing the language as variable into the function?
I also tried as $rootScope.getTourTranslation = function(key) but no luck
edit:
This is how the language variable is filled. We use angular-translate, so that is the $translate service. This code is placed above the getTourTranslation function.
$rootScope.changeLanguage = function (langKey) {
if(langKey){
if(langKey.length == 2) {
$translate.use(langKey.toLowerCase()+"_"+langKey.toUpperCase());
$rootScope.language = langKey;
} else if(langKey.length == 5) {
$translate.use(langKey);
$rootScope.language = langKey.substring(0,2);
}
}
};
$rootScope.$on('$translateChangeSuccess', function () {
if($translate.use()){
$rootScope.language = $translate.use().substring(0,2);
if($state.$current && !$state.$current.self.abstract) {
$state.reload();
}
}
});
$rootScope.changeLanguage($translate.use());
It is not possible that variable defined on $rootScope is not available withing the same context.
I can imagine only one case:
it calls getTourTranslation so early so none of changeLanguage or $translateChangeSuccess handler ran
The problem was, that $rootScope.language was called before it was initialized.
So, I placed the call to the getTourTranslation() function in an function which is called after firing an button. So, the right variables are now only initialized when they are needed instead of always.
So, I fixed two things, and that all based on the comment from #BotanMan
I have an edit page where the user can edit a file in the system, and then save it. When loading the file, I make two objects out of the result, one is bound to the view and the other I wish to keep (in its original state) until "save" is clicked, and then use it to compare vs the view-bound object, to see if any changes have been made.
So, when the page loads, this is being run
$http.get('/api/files/' + $stateParams.id)
.then(function (result) {
vm.fileTemp = result.data;
vm.fileTempCopy = result.data;
The fileTempCopy is not being touched or referenced by anything in the view or elsewhere in the controller, except in the save-method, where i check if they are alike or not. But somehow, both of them are updated when i make changes to the input fields (as if they were both used as ng-model for the inputs).
if(vm.fileTemp === vm.fileTempCopy)//in save-function
is always true, and their fields are exactly the same.
Why does this happen and how can I solve it?
Using the assignment operator, you are actually just referencing the original array. Arrays are reference types. That means, that they don't actually store values, they only store references to those values. What you where doing is copying a reference to a memory location, meaning that any changes to the memory at that location (including removing elements) will be reflected in both arrays.
So you will want to do this instead:
vm.fileTemp = angular.copy(result.data);
vm.fileTempCopy = angular.copy(result.data);
here is a very basic approach to checking an object's "value equality".
function isEquivalent(a, b) {
// Create arrays of property names
var aProps = Object.getOwnPropertyNames(a);
var bProps = Object.getOwnPropertyNames(b);
// If number of properties is different,
// objects are not equivalent
if (aProps.length != bProps.length) {
return false;
}
for (var i = 0; i < aProps.length; i++) {
var propName = aProps[i];
// If values of same property are not equal,
// objects are not equivalent
if (a[propName] !== b[propName]) {
return false;
}
}
// If we made it this far, objects
// are considered equivalent
return true;
}
//After your update Outputs: false
console.log(isEquivalent(vm.fileTemp, vm.fileTempCopy));
I am trying to call a loopback find function inside of a for loop, passing in a value from the iteration into the loopback function. The main issue of the code can be represented by the following:
for (var a = 0; a < $scope.countries.length; a++) {
$scope.getEmFacPurElec($scope.countries[a], 'ton/kWh', 'CO2e').then(function(result) {
emFacPurElecToUse = $scope.emFacPurElecs;
}
And here is the function being called:
$scope.getEmFacPurElec = function (country, unit, ghgType) {
var defer = $q.defer();
$scope.emFacPurElecs = [];
$scope.emFacPurElecs = Country.emFacPurElecs({
id: country.id,
filter: {
where: {
and: [
{unit: unit},
{ghgType: ghgType}
]
}
}
});
defer.resolve('Success getEmFacPurElec');
return defer.promise;
};
The problem is that the loopback promise function is called and then returned undefined which means that it moves to the next iteration of the for loop before getting the value to assign to emFacPurElecToUse. I need to do some more calculations with that variable for that country before moving to the next country.
I have looked at using $q.all as a possible solution and also using array.map as per http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html (Rookie mistake #2: WTF, how do I use forEach() with promises?), but I just cannot figure out how to pull it all together to make it work. Should I be using a forEach instead?
I also saw this link angular $q, How to chain multiple promises within and after a for-loop (along with other similar ones) but I do not have multiple promises that I need to process inside the for loop. I need to retrieve the value of one emFacPurElecs for that country, do some work with it, then move to the next country. I feel I am close but I just cannot get my head around how I would code this particular functionality. Any help is greatly appreciated.
It seems to me that you do have multiple promises to process inside your for loop, as you say "I need to do some more calculations with that variable for that country before moving to the next country." This should all be done with in the promise chain that I've suggested - calcEmFacPurElec.
$scope.calcEmFacPurElec = function (country, unit, ghgType) {
$scope.getEmFacPurElec(country, unit, ghgType).then(function(countryEmFacPurElecs) {
// do something with countryEmFacPurElecs
return countryEmFacPurElecs;
}
$scope.getEmFacPurElec = function (country, unit, ghgType) {
var defer = $q.defer();
defer.resolve(Country.emFacPurElecs({
id: country.id,
filter: {
where: {
and: [
{unit: unit},
{ghgType: ghgType}
]
}
}
}); );
return defer.promise;
};
Hopefully the above is a pointer in the right direction!
When you want to carry out a promise chain on an array of items, then as you have identified, Promise.all (using whatever promises implementation you require) is what you want. .all takes in an array of Promises, so in your for loop you can do:
var promises = [];
for (var a = 0; a < $scope.countries.length; a++) {
promises.push($scope.calcEmFacPurElec($scope.countries[a], 'ton/kWh', 'CO2e')); // new promise chain that does all of the work for that country
}
$q.all(promises).then(function(arrayofCountryEmFacPurElecs) {console.log('all countries completed')});
I currently have 2 pages, page1.php and page2.php, each of the pages uses a controller that completes its own functions etc.
However there are tabs within the pages that are exactly the same that gets a promise from a factory within the module. The lists are exactly the same except for querying on different IDs. For example both controllers have this:
pageListFactory.getData().then(function (result) {
$scope.BasicItems = result; $scope.basicItemsList = [];
angular.forEach($scope.BasicItems, function (BasicItem) {
angular.forEach(BasicItem['SomeInnerArray'], function (BasicSomeInnerItem) {
if (BasicSomeInnerItem == VARIABLE_THAT_CHANGES) {
$scope.basicItemsList.push({
ID: BasicItem.ID, Title: BasicItem.Title
});
}
});
});
});
So this code is used, and the VARIABLE_THAT_CHANGES is just what changes each time. However as I said it is used on multiple pages, is there a way to create just one function call and then each page just can call a specific bit and send the variable with it?
I tried using $rootScope but of course this just clogs and slows, and I'm not exactly happy on constantly using $rootScope to pass the $scope.basicItemsList around as the list could get quite big.
So is there any way to reuse this code without just copying and pasting it into each controller?
Sure you can re-use it...
Convert the factory to a service, its basically a name change, create a local variable to store the data, update the data on first call, and then grab the data if it exists on the second call.
.service('myService', ... stuff ... { // I suggest using a service, as I don't know if a factory would act as a singleton
var myData = null;
return {
getData: function(){
if(myData != null)
return myData; // returns data
else {
return $http()... // ajax call returns promise
}
},
setData: function(dataToSet){
myData = dataToSet;
}
}
Then your controllers:
//controller 1
var promiseOrData = pageListFactory.getData();
if(promiseOrData instanceOf Array){ // or whatever data you expect
$scope.BasicItems = promiseOrData;
....
}
else { // should be a promise
promiseOrData.then(function (result) {
pageListFactory.setData(result); // set the data once you get it.
$scope.BasicItems = result; $scope.basicItemsList = [];
....
}
}
In controller 2 you only need to get the data as the returned data will be an array, not a promise
On top of all this, write a directive which will process the data when you pass it along, then you can pass the variableThatChanges and have the directive take care of it.
Use services and write the function in that service and pass the variable VARIABLE_THAT_CHANGES into it. By this you can reuse the code.
I am new to Angular (and JS) and just a little confused.
I start a timer with :
var getOverviewMapTimer = $interval($scope.UpdateOverviewMap, UPDATE_FREQUENCY);
and, if I understand it, getOverviewMapTimer is a "promise".
I want to be able to check if the timer is running & had excepted that if I ever
$interval.cancel(getOverviewMapTimer); then getOverviewMapTimer would be null and I could check for that.
This seems not to be the case.
Do I have to explicitly destroy the promise (what goo is a promise to timer that has been cancelled?).? If so, how & would I then have to explicitly set it to null?
I think that I should use cancel(getOverviewMapTimer);, but am not 100% sure as getOverviewMapTimer is still non-null afterwards.
Thanks for your help
After var getOverviewMapTimer = $interval(...); getOverviewMapTimer holds a reference to an object (that happens to be a promise).
Doing $interval.cancel(getOverviewMapTimer) passes (and cancels) the object referenced by the getOverviewMapTimer variable, but there is no way to convert an object to null. The getOverviewMapTimer variable will continue to hold a reference to the promise object and the only way to set it to null is through a new assignment (i.e. explicitely setting it to null):
var getOverviewMapTimer = $interval(...);
...
$interval.cancel(getOverviewMapTimer);
getOverviewMapTimer = null;
Advanced Topic:
It sounds indeed nice to have an easy way to find out if an interval-promise has been cancelled or not (e.g. adding a custom cancelled property to the promise object and setting its value to true when cancelling the interval).
Angular is cool enough to be incredibly flexible and extensible, so using the concept of a Service Decorator we are able to "augment" the $interval service, extending its cancel() method to add a cancelled property and setting its value to true when cancelling an interval-promise:
/* Service Decorators can be configured in `config` blocks */
app.config(function ($provide) {
/* Register a decorator for the `$interval` service */
$provide.decorator('$interval', function ($delegate) {
/* Keep a reference to the original `cancel()` method */
var originalCancel = $delegate.cancel;
/* Define a new `cancel()` method */
$delegate.cancel = function (intervalPromise) {
/* First, call the original `cancel()` method */
var retValue = originalCancel(intervalPromise);
/* If the promise has been successfully cancelled,
* add a `cancelled` property (with value `true`) */
if (retValue && intervalPromise) {
intervalPromise.cancelled = true;
}
/* Return the value returned by the original method */
return retValue;
};
/* Return the original (but "augmented") service */
return $delegate;
});
});
Now, we can use the $interval service as usual and we can always check an interval-promise's cancelled property to find out if it has been cancelled.
var getOverviewMapTimer = $interval(...);
...
console.log(!!getOverviewMapTimer.cancelled); // false
$interval.cancel(getOverviewMapTimer);
console.log(!!getOverviewMapTimer.cancelled); // true
See, also, this short demo.
It's technically impossible that getOverviewMapTimer is null after calling $interval.cancel(getOverviewMapTimer);.
If you want it to be null you have to do it yourself. Either immediately after cancelling the interval or via callback:
if ($interval.cancel(getOverviewMapTimer)) {
getOverviewMapTimer = null;
}
or
var getOverviewMapTimer = $interval($scope.UpdateOverviewMap, UPDATE_FREQUENCY);
getOverviewMapTimer.catch(function() { getOverviewMapTimer = null; });
Be aware, though, that the callback is asynchronous.
easy
var interval = $interval(function() { }, 1000);
console.log(interval.$$state.status); //0
$interval.cancel(interval);
console.log(interval.$$state.status); //2
if(interval.$$state.status == 0)
//valid
else
//invalid