How to use asynchronous calls in a loop in angular? - angularjs

Each email in a list is to be sent to server and response to be be got from server that if it is a valid email.
So after all emails are checked the array should have :
joe#gmail.com - valid
abc#fjkdsl.com - invalid
xyz#yahoo.com - valid
test#nknk.com - invalid
The code to send the emails to server in a loop is like :
for(var i=0; i < isEmailValidList.length; i++) {
var isEmailValid = User.validateEmail({email : isEmailValidList[i].email}, function(){
isEmailValidList[i].isValid = isEmailValid.value;
});
}
But the problem is that the calls are asynchronous, so for say i=0, the control will not go inside the function when i is 0. So when it does go inside the function value of i can be anything, mostly it is greater than length of array, so isEmailValidList[i] is undefined. If the call were synchronous then it would have waited for response and i would not have been incremented, but this is not the case.
So, how do I get the correct isValid response for its corresponding email ?

Use promises. Angular can work with promises without any "special intervention", just like assigning a value to a scope variable, see the plnkr. Promises are the "base" to tame asynchronous programming to work like synchronous programming (while we don't have javascript generators in browsers) and is encouraged by Angular team because it's highly testable and maintainable
http://plnkr.co/edit/8BBS2a1kC24BHBWRYp9W?p=preview
// trying to emulate your service here
var app = angular.module('app', []);
app.factory('User', function($q, $timeout){
User = {};
User.validateEmail = function(email){
var d = $q.defer();
$timeout(function(){
if (/(yahoo|gmail)/.test(email.email)){
d.resolve(email); // return the original object, so you can access it's other properties, you could also modify the "email" object to have isValid = true, then resolve it
} else {
d.resolve(); // resolve it with an empty result
}
}, 1000, false);
return d.promise;
};
return User;
});
app.controller('MainCtrl', function(User, $q){
this.emails = [
{email: 'joe#gmail.com', name: 'Joe'},
{email: 'abc#fjkdsl.com', name: 'Abc'},
{email: 'xyz#yahoo.com', name: 'XYZ'},
{email: 'test#nknk.com', name: 'test'}
];
this.isEmailValidList = [];
var promises = [];
for(var i=0; i < this.emails.length; i++) {
promises.push(
User.validateEmail(this.emails[i])
);
}
$q.all(promises).then(function(emails){
this.isEmailValidList = emails.filter(function(e){ return e; });
}.bind(this));
});
Notes: The $timeout is to emulate an asynchronous task, like a database call, etc. You could pass the entire emails array to the validation service then return the array, instead of creating an intermediate array of promises. With angular, you may assign a scope variable to a promise, and you can use it on ng-repeat without any changes to the code

You can use closure function:
for (var i=0; i < isEmailValidList.length; i++) {
var isEmailValid = User.validateEmail({
email : isEmailValidList[i].email
}, (function(i) {
return function() {
isEmailValidList[i].isValid = isEmailValid.value;
};
})(i));
}

Related

AngularJS Promises: Put promise in Factory so it is globally accessible and *editable*

I'm trying to pull data from an external JSON file and display it for the user to see. Through various actions, the user would then be able to change the data returned from the JSON file, without writing those changes to the file (in this example, incrementing values by one by clicking on a div). I've created a promise service that successfully pulls the data and displays it. I can even get it so the data can be changed in individual controllers.
This is where I get stuck: I cannot find a way to make any changes to the data in the PromiseService, so changes cannot propagate globally. How do I make it that any change in the promise data at the controller level will be reflected in the PromiseService and, thus, reflected in any data binding in the app? I'm new to promises, so I'm open to a completely different approach.
Plunker
HTML:
<body ng-app="pageApp" ng-controller="pageCtrl" nd-model="items">
{{items}}
<div class="button" ng-controller="buttonCtrl" ng-click="incrementValues()">
Click to increment:
<br>{{items}}
</div>
</body>
PromiseService:
pageApp.factory('PromiseService', function($http) {
var getPromise = function() {
return $http.get('items.json').then(function(response) {
return response.data;
});
};
return {
getPromise: getPromise
};
});
Button Controller (Page Controller in Plunker):
pageApp.controller('buttonCtrl', function($scope, PromiseService) {
$scope.incrementValues = function()
{
PromiseService.getPromise().then(function(data) {
$scope.items = data;
for(var i = 0; i < data.items.length; i++)
{
data.items[i]['value']++;
}
}).catch(function() {
});
};
});
The incrementValues function works successfully the first time, but each consecutive click re-pulls the promise and resets the data. To sum up: how do I reflect the incremented values in the PromiseService, as opposed to local variables?
You could add to your factory a private property where you store the items. Then create 3 different methods to update and access to that property.
pageApp.factory('PromiseService', function($http) {
var items = {}; // [] in case it is an array
var updateData = function(updatedData){
items = updatedData;
}
var getUpdateData = function(){
return items;
}
var getPromise = function() {
return $http.get('items.json').then(function(response) {
items = response.data;
return response.data;
});
};
return {
getPromise: getPromise,
updateData : updateData,
getUpdateData : getUpdateData
};
});
pageApp.controller('buttonCtrl', function($scope, PromiseService) {
$scope.items = [];
//You should call this method to retrieve the data from the json file
$scope.getData = function(){
PromiseService.getPromise().then(function(data) {
$scope.items = data;
}).catch(function() {
});
}
$scope.incrementValues = function(){
for(var i = 0; i < $scope.items.length; i++){
$scope.items[i]['value']++;
}
PromiseService.updateData($scope.items); //This could be skipped in case you do not want to 'store' these changes.
};
});
Then in others controller you could use the same service to retrieve the updated Data like this:
$scope.items = PromiService.PromiseService();
In the future you could also create a new method to update the json itself instead of stored internally
Your function creates a new $http call every time it's called, and thus returns a new promise, encspsulating new data, every time it's called.
You need to return the same promise every time:
var thePromise = $http.get('items.json').then(function(response) {
return response.data;
});
var getPromise = function() {
return thePromise;
};

Enriching javascript objects contained in an angular promise with behavior/methods

I have the following angular service. It retrieves a array of json messages from the backend and I want to be able to convert those messages (plain js objects with no behavior) into object of type Message (see below).
Ideally I'd like to transform the data from the unresolved ng promise and passing each json message into the Message constructor as follows:
new Message(jsonMsg);
How can I achieve that?
Here is my service:
function Message(data) {
var defaults = {
sender: null,
recipient: null,
messageRead: false
};
angular.extend(this, defaults, data);
}
Message.prototype.getCounterparty = function (user) {
if (!this.sender) return null;
return (user.id !== this.sender.id) ? this.sender : this.recipient;
};
Message.prototype.isSender = function (user) {
return user.id === this.sender.id;
};
Message.prototype.isRecipient = function (user) {
return user.id === this.recipient.id;
};
...
findLatestsMessages: function (otherId) {
return $http.get('/api/message/find-latest-messages' + otherId);
}
You have to use a factory :
angular.factory('Message', [function() {
var Message= function(data) {
var defaults = {
sender: null,
recipient: null,
messageRead: false
};
angular.extend(this, defaults, data);
}
Message.prototype.isSender = function (user) {
return user.id === this.sender.id;
};
// ...
return Message;
}]);
Then you inject your factory in your controller
// ...
angular.controller('ctrl', ['$http', '$scope', 'Message', function($http, $scope, Message) {
$scope.messages = []
$scope.findLatestsMessages = function (otherId) {
return $http
.get('/api/message/find-latest-messages' + otherId)
.then(function(response) {
// and you create your messages when your promise has resolved
$scope.messages = response.data.map(function(m) {
return new Message(m);
}
});
}]);
// Assuming json response looks like
// [
// {sender: "John Doe", recipient: "Jane Doe", messageRead: true},
// {sender: "John Doe", recipient: "Jane Doe", messageRead: true}
// ]
findLatestsMessages: function (otherId) {
return $http
.get('/api/message/find-latest-messages' + otherId)
.then(function (response) {
var messages = response.data;
return messages.map(function (message) {
return new Message(message);
});
});
}
USAGE:
findLatestsMessages(someId).then(function (messages) {
// messages is an array of Message objects.
});
Then I will no longer have a promise to deal with on the calling-side?
You can return an object reference that later gets resolved to what you want. But beware, an XHR created by $http always resolves asynchronously, sometime after it is created.
As example:
function MessageResponseObject(url) {
var message = {};
var httpPromise = http.get(url);
//transform response
var derivedPromise = httpPromise.then( function(response) {
//save data to object
message.data = response.data;
//return message reference for chaining
return message;
});
//attach promise to object
message.$promise = derivedPromise;
//
return message;
});
$scope.message = MessageResponseObject(url);
You can use the response object in your template without a promise.
<div ng-repeat="item in message.data">
{{item}}
</div>
This will work in the UI because the AngularJS framework puts a $watchCollection on message.data. It checks the value after each digest cycle; and when the data gets resolved, it places the data on the DOM.
Clients in the controller that need to wait can use the attached promise.
$scope.message.$promise.then (function onFulfilledHandler(message) {
//do subsequent actions
}).catch (function onRejected(errorResponse) {
//do error actions
});
It is important to realize that using method immediately returns an empty reference. Once the data is returned from the server the existing reference is populated with the actual data. This is a useful trick since usually the resource is assigned to a model which is then rendered by the view. Having an empty object results in no rendering, once the data arrives from the server then the object is populated with the data and the view automatically re-renders itself showing the new data. This means that in most cases one never has to write a callback function for the action methods.1
Best practice
Return the promise as a property of the object that resolves to the object. This makes it easy to use in resolve section of $routeProvider.when() to defer view rendering until the resource(s) are loaded.
For more information on this methodology, see AngularJS ngResource $resource API Reference.

Angular fill and return service var on controller call

I am struggling with this for a while, and can't figure it out. What I have is main controller, factory, and service, and I'm trying to store array from service to $scope in controller. On view on item click this function in controller is triggered:
$scope.getSongsInPlaylist = function()
{
var playlistId = this.item.idPlayliste;
$scope.Mp3Files = songInPlaylistFactory.getSongsForPlaylist(playlistId);
}
That works ok, this function retrieves item from view and sends id of that item to function in service.
Than in my service I have code like this:
var Songs = [ ];
this.getSongsForPlaylist = function (id)
{
for (var i = 0; i < SongIsOnPlaylist.length; i++)
{
if(SongIsOnPlaylist[i].idPlayliste == id)
{
dataFactory.getDataById(mp3fileUrl, SongIsOnPlaylist[i].idPjesme)
.success(function (data) {
Songs.push(data);
alert(Songs[0].naslovPjesme);//Alert one
});
}
}
alert(Songs[0]);// Alert two
return Songs;
}
dataFactory is my factory that communicates with api in backend, and that works too. var Songs is defined as: var Songs = [ ]; And SongIsOnPlaylist is filled with data.
When I trigger this, alert two gives me undefined, and alert one gives me name of first song in Songs. That means var Songs is filled with data, but when I want it to return to controller its empty ...
Am I doing something wrong here, I would appreciate any help?
First it looks like your dataFactory.getDataById is async call.
Because of this what is actually happening is you returns an empty Songs array before it gets populated when all your async calls returns.
To solve this I would advise to use a Promise library like bluebird to do something like this:
// note that now your service will return a promise
this.getSongsForPlaylist = function (id) {
return new Promise(function(resolve, reject) {
var promises = [];
// here in your loop just populate an array with your promises
for (var i = 0; i < SongIsOnPlaylist.length; i++){
if(SongIsOnPlaylist[i].idPlayliste == id){
promises.push( dataFactory.getDataById(mp3fileUrl, SongIsOnPlaylist[i].idPjesme) )
}
}
// now use the library to resolve all promises
Promise.all(promises).then( function (results) {
//HERE YOU WILL GET the results off all your async calls
// parse the results
// prepare the array with songs
// and call
resolve(Songs);
});
});
}
Then you will use the service like this:
$scope.getSongsInPlaylist = function() {
var playlistId = this.item.idPlayliste;
songInPlaylistFactory.getSongsForPlaylist(playlistId)
.then(function(Songs){
$scope.Mp3Files = Songs
})
.error(function(err){
//here handle any error
});
}

How to communicate with server using AngularJS within Google Apps Script

Recently it has become possible to use angularjs within google apps script via the iframe sandbox mode.
My problem comes when trying to communicate with the server (gapps spreadsheet) and receiving asynchronous data in return.
The implementation for receiving data from the server is to use a function with a callback function like so:
google.script.run.withSuccessHandler(dataGatheringFunction).getServerData();
getServerData() would be a function that resides server-side that would return some data, usually from the accompanying spreadsheet. My question is how to use the callback function within the parameters of AngularJS. A typical $http function could be placed in a provider, and the scope value could be populated after then.() returns. I could also invoke $q. But how would I deal with the necessity of google's callback?
Here's a simplified version of what I'm messing with so far:
app.factory("myFactory", function($q){
function ssData(){
var TssData = function(z){
return z;
}
google.script.run.withSuccessHandler(TssData).getServerData();
var deferred = $q.defer();
var d = deferred.resolve(TssData)
console.log("DP: " + deferred.promise);
return deferred.promise;
}
return ssData();
})
Then in the controller resolve the server call similar to this:
myFactory.then(set some variables here with the return data)
My question is simply - How do I deal with that callback function in the provider?
The script throws no errors, but does not return the data from the server. I could use the old $timeout trick to retrieve the data, but there should be a better way.
You only need to $apply the output from the server function:
google.script.run.withSuccessHandler(function(data) {
$scope.$apply(function () {
$scope.data = data;
});
}).withFailureHandler(errorHandler).serverFunction();
Maybe the most elegant solution that makes sure the google.script.run callbacks are registered automatically in the AngularJS digest cycle would be to use the $q constructor to promisify the google callbacks. So, using your example above:
app.factory('myFactory', ['$q', function ($q){
return {ssData: ssData};
function ssData(){
var TssData = function(z){
return z;
};
var NoData = function(error) {
// Error Handling Here
};
return $q(function(resolve, reject) {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
.getServerData();
}).then(TssData).catch(NoData);
}
}]);
Then in your controller you can call myFactory.ssData()
Since I don't know exactly what TssData is doing I included it here but note that this simply returns another promise in this context which you will still have to handle in your controller:
myFactory.ssData().then(function(response) {
// Set data to the scope or whatever you want
});
Alternately, you could expose TssData by adding it to the factory's functions if it is doing some kind of data transformation. If it is truly just returning the response, you could refactor the code and omit TssData and NoData and handle the promise entirely in the controller:
app.factory('myFactory', ['$q', function ($q){
return {ssData: ssData};
function ssData(){
return $q(function(resolve, reject) {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
.getServerData();
});
}
}]);
app.controller('myController', ['myFactory', function(myFactory) {
var vm = this;
myFactory.ssData()
.then(function(response) {
vm.myData = response;
}).catch(function(error) {
// Handle Any Errors
});
}]);
An excellent article about promises (in Angular and otherwise) is here: http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
This guy seems to be pulling data from a GSheet into angular quite happily without having to do anything fancy.
function gotData(res) {
$scope.validUser = res.validUser;
var data = angular.copy(res.data), obj, i=0;
Object.keys(data).forEach(function(sh) {
obj = {title: sh, checked: {}, showFilters: false, search: {}, sort: {index: 0, reverse: false}, currentPage: 0, checkedAll: true, showBtns: true, searchAll: ''};
obj.heading = data[sh].shift();
obj.list = data[sh];
obj.heading.forEach(function(s,i) {
obj.checked[i] = true;
});
$scope.sheets.push(obj);
});
$scope.sheets.sort(function(a,b) {
return a.title > b.title ? 1 : -1;
});
$scope.gotData = true;
$scope.$apply();
}
google.script.run.withSuccessHandler(gotData).withFailureHandler($scope.gotError).getData();
My solution was to get rid of the $q, promise scenario all together. I used $rootScope.$broadcast to update scope variables from the server.
Link to spreadsheet with script.

Fire event from a service without "polluting" the $rootScope

I'm building an app in angularjs, where I have a central notification queue. Any controller can push into the queue and digest the messages.
I have built a service like:
angular.module('app').factory('notificationSvc', ['translateSvc', notification]);
function notification(translate) {
var notificationQ = [];
var service = {
add: add,
getAll: getAll
};
return service;
function add(message, type) {
notificationQ.push({
message: message,
type: type
});
}
function getAll() {
return notificationQ;
}
}
(One of the problems with this is that the notificationQ can be modified unsafely by calling svc.getAll()[3].message = "I have changed a message"; or something similar. I originally wanted a "push only" service with immutable messages, but this problem is outside of the scope of this question.)
If I digest this queue in a controller like:
$scope.notifications = svc.getAll();
$scope.current= 0; // currently visible in the panel
And use it like:
<div ng-repeat="notification in notifications" ng-show="$index == current">
<p>{{notification.message}}</p>
</div>
I can bind to it, see it changing and all is well. I can cycle through past notifications by changing the variable current.
The question:
When the queue gets a new element I want the $scope.index variable to change to notifications.length - 1. How do I do that?
I have seen examples using $rootScope.$broadcast('notificationsChanged'); and $scope.$on('notificationsChanged', function() { $scope.index = $scope.notifications.length - 1; });, but I did not really like the pattern.
I have a controller that knows about the service, has a direct reference to it, and yet we use $rootScope to communicate? Everything else sees the $rootScope, and all the events from different services will clutter up there.
Can't I just put the event on the service instead? Something like this.$broadcast('notificationsChanged') in the service and svc.$on('notificationsChanged', function() { ... }); in the controller.
Or would it be cleaner to watch the data directly? If yes, how? I don't like this as I was not planning on exposing the full array directly (I was planning on get(index) methods) it just sort of happened along the lines where I had no idea what I was doing and was happy that at least something works.
You could just manage events yourself. For example (untested):
function EventManager() {
var subscribers = [];
var service = {
subscribe: subscribe;
unsubscribe: unsubscribe;
publish: publish
}
return service;
function subscribe(f) {
subscribers.push(f);
return function() { unsubscribe(f); };
}
function unsubscribe(f) {
var index = subscribers.indexOf(f);
if (index > -1)
subscribers.splice(index, 1);
}
function publish(e) {
for (var i = 0; i < subscribers.length; i++) {
subscribers[i](e);
}
}
}
function notification(translate) {
var notificationQ = [];
var addEvent = new EventManager();
var service = {
add: add,
getAll: getAll,
onAdded: addEvent.subscribe;
};
return service;
function add(message, type) {
var notification = {
message: message,
type: type
};
notificationQ.push(notification);
addEvent.publish(notification);
}
function getAll() {
return notificationQ;
}
}
Then, from your controller:
...
var unsubscribe = notificationSvc.onAdded(function(n) { /* update */ });
Caveat: using this method the service will maintain a reference to the subscriber function that is passed to it using subscribe, so you have to manage the subscription using $scope.$on('$destroy', unsubscribe)
The notification approach would definitely work. Depending on your implementation it would be the right solution.
Another approach would be to watch the notifications array in your controller, like this:
$scope.$watchCollection('notifications', function(newValue, oldValue) {
$scope.index = newValue.length - 1;
});
This should work, because your controller receives a direct reference to the notifications array and therefore can watch it directly for changes.
As runTarm pointed out in the comments, you could also directly $watch the length of the array. If you're only interested in length changes this would be a more memory saving approach (since you don't need to watch the whole collection):
$scope.$watch('notifications.length', function (newLength) {
$scope.index = newLength - 1;
});

Resources