I need to pass some local parameter to the $q.all callback
var actions = [];
var jsonFiles = ["a.json","b.json","c.json"];
for(var index=0; index<3; index++){
actions.push($http.get('content/' + jsonFiles[index]);
}
$q.all(actions).then(function (values) {
console.log(index) // Need to print current request index
}
current output is of course 3,3,3
I need to print 0,1,2 according to the response order (it can be 1,0,2 or other combination)
I've created a jsfiddle with my issue - http://jsfiddle.net/dorcohen/n30er4ap/
If I understand you correctly , you should use params :
for (var index = 0; index < 3; index++)
{
actions.push($http.get( jsonFiles[index], {params:{"idx": index }}));
}
Then :
$q.all(actions).then(function(values) {
for (var i=0;i<values.length;i++)
$scope.indexes.push(values[i].config.params.idx);
})
Fiddle
OK, so it is rather overkill, but it will work I think
for(var index=0; index<3; index++){
actions.push($q.all([
$q.resolve(index),
$http.get('content/' + jsonFiles[index]);
]);
}
While #Royi's answer is correct, if will not work for "non-http" promises.
A nice workaround which will work for kind of promises is by using the anti-pattern of creating a defered object as a wrapper and resolving your own custom object.
While this is a anti-pattern, there are some cases you want to use it like here.
HTML:
<div ng-app="app">
<div ng-controller="ctrl">
<button ng-click="do()">
Click
</button>
</div>
</div>
JS:
angular.module('app', []).
controller('ctrl', function($scope, $http, $q) {
$scope.do = function() {
var acts = [];
for (var i = 0; i < 3; i++) {
var defer = $q.defer(); // create your own deferred object
// worked with a dummy promise - you can use what ever promise you want
var promise = $q.when({
obj: i
});
promise.then(
// closure for the current deferred obj + index since we are in a loop
function(d,idx) {
return function(item) {
d.resolve({res: item, index:idx}); // resolve my deferred object when I want and create a complex object with the index and the "real response"
}
}(defer,i));
acts.push(defer.promise);
}
$q.all(acts).then(function(res) {
console.log(res);
});
}
});
JSFIDDLE.
Related
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;
};
i'm trying to update a scope list inside a callback function. This apparently works fine, but, after some seconds, console gets error: [$rootScope:infdig]. I tried to disable two-way databinding, but, the error continues.
Controller:
app.controller('ChapterCtrl', function ($rootScope, $scope, Services, chapter) {
$rootScope.headerTitle = chapter.name;
$scope.terms = [];
cctdbterms.webdb.getTermsByChapter(chapter.id, function(tx, results) {
$scope.terms = results.rows;
$scope.$apply();
});
});
View:
<div class="view" ng-repeat="term in terms">
<div ng-bind-html="term.description"></div>
</div>
The answer that a find is seraching is: "Problem was that the filter was providing a different array each time hence causing a loop" from Why do I get an infdig error?
Thinking about it, I solved in a simple way, interating my returning list:
app.controller('ChapterCtrl', function ($rootScope, $scope, Services, chapter) {
$rootScope.headerTitle = chapter.name;
$scope.terms = [];
cctdbterms.webdb.getTermsByChapter(chapter.id, function(tx, results) {
for (var i = 0; i < results.rows.length; i++) {
var term = results.rows[i];
$scope.terms.push(term);
}
$scope.$apply();
});
});
I have an array of links, which I get in first request. My goal is to go to every link to gather data. So I want to make a promise for every request, push them all into an array and then pass to Q.all to resolve all the promises. The problem is I can't return promise and go to the next link
Here is the function, where I tried to make multiple requests and gather data
function arrayPromise(linksArr){
function collectingData(elem){
var deferredNew = Q.defer();
var url = elem;
request(url, function(error,response,html){
if(error){
deferredNew.reject(error);
}
var $ = cheerio.load(html);
var title, content;
$('.entry-title').filter(function(){
var data = $(this);
var title = data.text();
items.text.push(
{ titleof: title }
)
})
$('.entry-content ').filter(function(){
var data = $(this);
var content = data.html();
items.text.push(
{ contentof: content})
})
deferredNew.resolve(items);
})
console.log("Returning the promise");
return defferedNew.promise;
}
var promiseArr;
console.log("LENGTH:");
console.log(linksArr.length);
for (var i = 0; i < linksArr.length; i++) {
console.log(linksArr[i]);
var tempPromise = collectingData(linksArr[i]);
console.log(tempPromise);
promiseArr.push(tempPromise);
};
return promiseArr;
}
And how I try to use it
var linksPromise = fetchLinks();
linksPromise.then(function(arr){
console.log("LINKS PROMISE RESOLVED");
Q.all(arrayPromise(arr)).then(function(data){
console.log("SUCCESS RESOLVING ALL PROMISES")
console.log(data);
},function(err){
console.log("ERROR RESOLVING ALL PROMISES", err);
});
},function(err){
console.log(err);
})
promiseArr should be declared as an array:
var promiseArr = [];
If that doesn't fix it, please provide the error that you might be seeing.
There are MULTIPLE problems
First is in
deferredNew.resolve(items);
items is defined in a local scopes not defined anywhere in scope where deferredNew.resolve(items); evaluated.
Another: Assigning empty array to promiseArr would help too.
One more: request(url, function(error,response,html) is not assigning result anywhere and your function has no return statement where you think you return promice deferredNew.resolve(items);
PS
There are more erros, check that all your function return value, for example $('..').filter(...) does not reurn values
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));
}
I built an infinite scroll for a mobile web app built with AngularJS with the following extras features:
I built it to be bidirectional
This is for a mobile web app so I wanted it to unload out-of-view contents to avoid memory issues
Here is the jsfiddle link.
Now, I have a few questions and I also needs a small code review:
I am not familiar with promises, but then() seems to be executed before $digest. Thus, I need to delay my codes with $timeout. For me, it's a sign that something is wrong. I would like to remove the $timeout on lines 85 and 98. The $timeout on line 85 is a bit "hacky", I need to make sure it is executed ms after then() otherwise, it won't work and I don't know why.
I would like to know if it's considered a "good practice" to call a $scope method from a directive. In my code, I am calling $scope.init(value) from my directive.
Including jQuery for a position() is quite funny. Should I be using a services with a function that does what $.position() does?
I know those could be seperate questions but they are really related to my piece of code.
For those who do not want to click on the jsfiddle link, here is the code:
HTML:
<div id="fixed" scroll-watch="4" scroll-up="loadTop()" scroll-down="loadBottom()">
<ul>
<li data-id="{{i.id}}" ng-repeat="i in items" ng-class="calculateType(i.id)">{{i.id}}</li>
</ul>
</div>
JS:
function Main($scope, $timeout, $q) {
var cleanup = 5;
$scope.items = [];
//This is called from the scrollWatch directive. IMO, this shouldn't be a good idea
$scope.init = function(value) {
var deferred = $q.defer();
//This $timeout is used to simulate an Ajax call so I will keep it there
$timeout(function() {
$scope.items = [{id: +value}];
$scope.loadTop();
$scope.loadBottom();
deferred.resolve();
}, 200);
return deferred.promise;
};
//This is only used to simulate different content's heights
$scope.calculateType = function(type) {
return 'type-' + Math.abs(type) % 4;
};
$scope.loadBottom = function() {
var deferred = $q.defer(),
counter;
if ($scope.items.length > 1) {
$scope.items.splice(0, cleanup);
}
//This $timeout is used to simulate an Ajax call so I will keep it there
$timeout(function() {
counter = (($scope.items[$scope.items.length - 1]) || {id: 0}).id;
for (var i = 1; i < 6; i++) {
$scope.items.push({id: counter + i});
}
deferred.resolve();
}, 200);
return deferred.promise;
};
$scope.loadTop = function() {
var deferred = $q.defer(),
counter;
//Why can't I use this here?
//$scope.items.splice($scope.items.length-cleanup, $scope.items.length);
//This $timeout is used to simulate an Ajax call so I will keep it there
$timeout(function() {
counter = (($scope.items[0]) || {id: 0}).id;
for (var i = 1; i < 6; i++) {
$scope.items.unshift({id: counter - i});
}
deferred.resolve();
}, 200);
return deferred.promise;
};
//Why is this method needs to be delayed inside the directive? I would like to call it in loadTop()
$scope.removeBottom = function() {
$scope.items.splice($scope.items.length-cleanup, $scope.items.length);
};
}
angular.module('scroll', []).directive('scrollWatch', ['$timeout', function($timeout) {
var lastScrollTop = 0;
return function($scope, elm, attr) {
var raw = elm[0];
$scope.init(attr.scrollWatch).then(function() {
//Why do I need this? It looks like the resolve is called before the $digest cycle
$timeout(function() {
raw.scrollTop = $('li[data-id="' + attr.scrollWatch + '"]').position().top;
}, 300); //This value needs to be great enough so it is executed after the $scope.loadTop()'s resolve, for now, I know that I can set it to 300 but in real life app?
});
elm.bind('scroll', function() {
if (raw.scrollTop > lastScrollTop && raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
$scope.$apply(attr.scrollDown);
} else if (raw.scrollTop < lastScrollTop && raw.scrollTop === 0) {
var scrollHeight = raw.scrollHeight;
$scope.$apply(attr.scrollUp).then(function() {
//Why do I need this? It looks like the resolve is called before the $digest cycle
$timeout(function() {
raw.scrollTop = raw.scrollHeight - scrollHeight;
//I would like to move this in the $scope.loadTop()
$scope.removeBottom();
});
});
}
lastScrollTop = raw.scrollTop;
});
};
}]);
Thank you
http://www.youtube.com/watch?v=o84ryzNp36Q
Is a great video on Promises, how to write them and how they work.
https://github.com/stackfull/angular-virtual-scroll
Is a directive replacement for ng-repeat that doesn't load anything not on screen It does from what I can tell exactly what your looking for.
I would have put this as a comment but you need 50 cred or reputation or whatever they call it.