I have an array of arrays that I want to run a async. function on.
for(var a in As){
this.doSomething(a).then(function(result){
for(var b in Bs){
this.somthingElse(b).then(function(){console.log(result)});
}
})
}
How can I force this to be processed in synchronously? I found Array.prototype.map and Array.prototype.reduce, but they seem not to be available in AngularJS?
Firstly, while AngularJS does not include a angular.map or angular.reduce, Array.prototype.map and Array.prototype.reduce is largely unrelated to AngularJS.
Any modern browser would support the map and reduce array functions. angular.forEach is for compatibility purposes (for IE8 and earlier). If you want to use map/reduce and supporting IE8 or earlier, try underscore.js or lodash.
Back to your question, there is not enough information on what you want to achieve and what actually the variables As and Bs are.
Let's make the following assumptions:
As and Bs are both proper JavaScript Arrays
For every memeber of As, you want to doSomething with it, wait until it's done, then do somthingElse with every member from Bs, when that's done, log the result of doSomething
This means you'll run somthingElse As.length * Bs.length times.
You do not care which As member has to be done first
This means As members would concurrently be done together
Similarly, for every Bs iteration, you do not care who is done first
Below is the code, you will need Angular's $q service:
// We cannot bring 'this' into map function's scope, so assign it to 'self'
var self = this;
var promises = As.map(function(a){
return self.doSomething(a).then(function(resultOfA){
return $q.all(Bs.map(function(b){
return self.somthingElse(b).then(function(resultOfB){
console.log(resultOfA);
return resultOfB; // Not neccessary
});
}))
});
});
// Now if you want to do something after all those iterations, do it here:
$q.all(promises).then(function(){
// do something after all 'As.length * Bs.length' iterations are done
});
You will see in the console log random sequences of resultOfA, implying they are done asynchronously. If you want get the actual results for use later, you can do this instead:
// We cannot bring 'this' into map function's scope, so assign it to 'self'
var self = this;
var promises = As.map(function(a){
return self.doSomething(a).then(function(resultOfA){
return $q.all(Bs.map(self.somthingElse)).then(function(resultsOfB){
return {
resultOfA: resultOfA,
resultsOfB: resultsOfB
};
});
})
});
// Now if you want to do something after all those iterations, do it here:
$q.all(promises).then(function(resultsOfA){
// do something after all 'As.length * Bs.length' iterations are done
console.log(resultsOfA);
// Output would be something like:
// [{
// resultOfA: 'Result of A[0]',
// resultsOfB: ['Result of B[0]', 'Result of B[1]', 'Result of B[2]...']
// }, {
// resultOfA: 'Result of A[1]',
// resultsOfB: ['Result of B[0]', 'Result of B[1]', 'Result of B[2]...']
// }]
});
you can create an array of promises and then use $q.all() to resolve all promises when they are all completed.
var promises = [];
for(var a in As){
promises.push(this.doSomething(a));
}
$q.all(promises).then(function(result){
for(var b in Bs){
this.somthingElse(b).then(function(){console.log(result)});
}
})
I think you're looking for a way to run everything synchronously - so if you have the following:
var As = ['A1', 'A2', 'A3'];
var Bs = ['B1', 'B2', 'B3'];
You'd want the execution to be 'A1', 'B1', 'B2', 'B3', 'A2', 'B1', ...
Is that correct?
If so, the following could be used:
function start(){
$log.log('start');
As.reduce(function(promise, itemA){
return promise.then(function(){
return doSomething(itemA).then(startBs);
});
}, $q.resolve());
}
function startBs(){
$log.log('start Bs');
return Bs.reduce(function(promise, itemB){
return promise.then(function(){
return somethingElse(itemB);
});
}, $q.resolve());
}
Sample plunker: https://plnkr.co/edit/fgiI3J2ylcW4FUXuXjIP?p=preview
if you are ok using bluebird, you can do something like this:
function doSomething(v){
return new Promise(function(fullfil, reject){
setTimeout(function(){
console.log(v);
fullfil(v);
},500);
});
};
var As = [[1,2],[3,4],[5,6]];
(function start(){
return Promise.map(As, function(a) {
return doSomething(a).then(function(){
return Promise.map(a, function(b) {
return doSomething(b);
});
});
});
})();
Related
I am writing an E2E test with protractor. I had to fetch information from the browser and execute a step multiple times.
I am testing one screen which will start when a
User clicks 'Start'
lands on a new page
The workflow below is invoked with count being passed as argument
id the html id does not change. the value changes when queried again after submitting the current form.
for(i = 0 ; i < count ; i++){
console.log("counter is "+i);
element(by('id')).evaluate('value').then(function(v) {
// do some action on UI based on v
element(by('id1')).sendKeys(v+v);
// submit etc.,
// some angular code runs in the frontend.
}
// need to wait since webdriver jumps to the next one without this completing
}
Many blog posts/documentations suggests you cannot use it in a loop, but does not suggest any alternative way to do this.
Any suggestions appreciated.
Never use protractor element statements inside loop: The simple reason is that the webdriverJS (protractor) API is asynchronous. Element statements returns a promise and that promise is in unresolved state while the code below the statements continues to execute. This leads to unpredictable results. Hence, it is advisable to use recursive functions instead of loops.
source: http://engineering.wingify.com/posts/angularapp-e2e-testing-with-protractor/
Edit: updated question with details of workflow.
It is usually not recommended to use a loop when an iteration has an asynchronous call.
The reason is that the first asynchronous calls is executed after the last iteration of the loop when i is already equal to count.
Thus, it makes it difficult to break the loop and to keep track of the value of i.
On way to tackle the issue is to use a recursive function :
var count = 3;
var results = [];
function iterate(i, n) {
if(i < n) {
console.log(`counter is ${i}`);
browser.refresh();
return element(by.id('h-top-questions')).getText().then(function(text) {
results.push(`${i}:${text}`);
return iterate(i + 1, n);
});
}
}
iterate(0, count).then(function(){
console.log("done!", results);
});
But a better way would be to iterate with promise.map on an array sized to the number of iterations:
var count = 3;
protractor.promise.map(Array(count).fill(0), function(v, i) {
console.log(`counter is ${i}`);
browser.refresh();
return element(by.id('h-top-questions')).getText().then(function(text) {
return `${i}:${text}`;
});
}).then(function(results){
console.log("done!", results);
});
You could also keep using a loop. First you'll have to use the let statement to get the value of i in an asynchronous function (ES6).
Then call all the synchronous code with browser.call to synchronize the execution:
var count = 3;
var results = [];
for(let i = 0 ; i < count ; i++){
browser.call(function(){
console.log(`counter is ${i}`);
browser.refresh();
element(by.id('h-top-questions')).getText().then(function(text) {
results.push(`${i}:${text}`);
});
});
}
browser.call(function() {
console.log("done!", results);
});
Looping in protractor works like this
describe('Describe something', function() {
var testParams = [1,2,3,4,5,6,7,8,9,10];
beforeEach( function() {
// ...
});
for (var i = 0; i < testParams.length; i++) {
(function (testSpec) {
it('should do something', function() {
// inside loop
});
})(testParams[i]);
};
});
Edit : I might be mis-understanding your question, but it seems to me you want to complete all(dynamic count) actions on the page, before going to the next one ?
it('should clear old inspections', function() {
inspectieModuleInspectieFixture.getRemoveInspectionButton().count().then(function (value) {
if(value == 0){
console.log('--- no inspections to remove ---');
}
for(var i = 0; i < value; i++){
//global.waitForClickable(inspectieModuleInspectieFixture.getRemoveInspectionButtonList(i+1));
inspectieModuleInspectieFixture.getRemoveInspectionButtonList(i+1).click();
console.log('iteration '+i + 'count '+value )
};
});
global.wait(5000);
}); */
this counts elements on the page and then it performs an action for the ammount of elements it found
In the above example I use containers to hold my elements, so my code remains readable (i.e. inspectieModuleInspectieFixture.getRemoveInspectionButton() holds $(".elementSelectorExample")
There is also a 'global.waitForClickable' commented, that is reffering to a 'time module' I've created that extends the functionality of 'wait', in this case it waits till the element is vissible/clickable.
This is easily mirrored perhaps something like this :
waitForElementNoDisplay: function(element){
return browser.wait(function() {
return element.isDisplayed().then(function(present) {
return !present;
})
});
},
this will make protractor WAIT untill an element is no longer displayed.(Display:none)
If you need to perform some action on every element, it is true, that better to not use loops. Use .map() or .each() or .filter() instead
Still not quite sure what you what to do, but here is example how i am doing similar tasks, when you need to make number of actions depending on data from the page:
class SomePage {
typeValueForEachElement(elements) {
elements.each((elem, index)=> {
elem.getAttribute('value').then(value=> {
elem.sendKeys(value + value)
elem.submit()
})
})
}
}
new SomePage().typeValueForEachElement($$('your locator here'))
Here is api reference that might help
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.map
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.reduce
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.each
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.filter
I'm having a problem getting at values in my service from the controller. My service looks like this:
angular.module('someApp').factory('someSvc', SomeSvc);
function SomeSvc($http) {
var colors = [];
function loadColors() {
return $http.get('SomeApi/GetColors')
.then(function (result) {
//colors = result.data.colors;//<-this doesn't work
//angular.copy(result.data.colors, colors);//<-this works
});
}
return {
loadColors: loadColors,
colors: colors
};
}
Then my controller might make a call like this:
someSvc.loadColors().then(function(){vm.colors = someSvc.colors;});
So, when I debug, if I set a breakpoint in the controller where the assignment to vm.colors is made, the colors property exposed on the someService object has just an empty array or any array with the expected values depending on which of the two commented-out lines I use in the service.
If I set a breakpoint in the service where the assignment to colors is made, the variable colors always has the expected values (e.g., let's say ["red", "yellow", "green"] is what comes back from the http call). So I can watch the controller trigger the http call, watch the value come back and get assigned to colors in the service, but then the controller just sees an empty array unless I do that angular.copy call.
Also, interestingly, if I change the service's return statement to look like this:
return {
loadColors: loadColors,
colors: function() {return colors;}
};
and then in the controller say vm.colors = someSvc.colors(); then that works just fine as well.
Why is this? Why isn't that array getting passed through?
UPDATE:
I've found that instead of the angular.copy() line, I can alternatively do this, and everything works as expected:
for (var i = 0; i < result.data.colors.length; i++) {
colors[i] = result.data.colors[i];
}
It seems to be that ASSIGNING the object is a problem, while modifying it is ok? Why is that?
This might work for ya. Guessing it's just a pointer issue maybe?
angular.module('someApp')
.factory('someSvc', function($http)
{
return {
colors: [],
loadColors: function()
{
var self = this;
return $http.get('SomeApi/GetColors').then(function (result)
{
self.colors = result.data.colors;
});
}
};
});
At the time you're calling return in your Factory, someSvc.colors is just the empty array - and the value is returned. Since Angular providers in general attempt to run only once, in future it doesn't actually check someSvc.colors again - just returns the initial value.
Wrapping it in a function means it runs the function every time, so it fetches the updated value.
This might be a really silly question but I couldn't find an answer anywhere else.
I want to unit test a service that for now has two simple operations:
angular.module('transaction').factory('Transaction',
function() {
var transactionList = [];
// Public API
return {
addTransaction: function(transaction) {
transactionList.push(transaction);
},
getTransactions: function(){
return transactionList;
}
};
}
);
Tests
Now in my tests I want to test if the service's addTransaction and getTransactions methods work, but I'm not really sure how to do it properly, because what I'm doing right now is using the other method to test the first, e.g:
it('should be able to return an array of transactions', function() {
//add new transaction
var trans = {id: 1, value: test};
Transaction.addTransaction(trans);
//test get
var result = Transaction.getTransactions();
expect(Array.isArray(result)).toBe(true);
expect(result).toEqual([trans]);
});
it('should be able to add a transaction', function(){
var trans = {id: 1, value: test};
Transaction.addTransaction(trans);
var result = Transaction.getTransactions();
expect(result).toEqual([trans]);
});
I expect these methods to become more complex, but what I wanted to do would be to have a way to test one of the methods without having to use the other. It would be great if anyone could point me to an example of some tests for operations similar to this.
I can see two ways for testing these independently:
1) make transactionList publicly available:
Transaction.addTransaction(trans);
expect(Transaction.transactionList).toEqual([trans]);
2) return transactionList in both methods
expect(Transaction.addTransaction(trans)).toEqual([trans]);
I would go with the second solution, as you can keep transactionList private.
I have:
$scope.bounds = {}
And later in my code:
$scope.$on('leafletDirectiveMap.load', function(){
console.log('runs');
Dajaxice.async.hello($scope.populate, {
'west' : $scope.bounds.southWest.lng,
'east': $scope.bounds.northEast.lng,
'north' : $scope.bounds.northEast.lat,
'south': $scope.bounds.southWest.lat,
});
});
The bounds as you can see at the begging they are empty but they are loaded later (some milliseconds) with a javascript library (leaflet angular). However the $scope.$on(...) runs before the bounds have been set so the 'west' : $scope.bounds.southWest.lng, returns an error with an undefined variable.
What I want to do is to wait the bounds (southWest and northEast) to have been set and then run the Dajaxice.async.hello(...).
So I need something like "wait until bounds are set".
You can use $watch for this purpose, something like this:
$scope.$on('leafletDirectiveMap.load', function(){
$scope.$watch( "bounds" , function(n,o){
if(n==o) return;
Dajaxice.async.hello($scope.populate, {
'west' : $scope.bounds.southWest.lng,
'east': $scope.bounds.northEast.lng,
'north' : $scope.bounds.northEast.lat,
'south': $scope.bounds.southWest.lat,
});
},true);
});
If you want to do this every time the bounds change, you should just use a $watch expression:
$scope.$watch('bounds',function(newBounds) {
...
});
If you only want to do it the first time the bounds are set, you should stop watching after you've done your thing:
var stopWatching = $scope.$watch('bounds',function(newBounds) {
if(newBounds.southWest) {
...
stopWatching();
}
});
You can see it in action here: http://plnkr.co/edit/nTKx1uwsAEalc7Zgss2r?p=preview
After upgrading to 1.2, promises returned by my services behave differently...
Simple service myDates:
getDates: function () {
var deferred = $q.defer();
$http.get(aGoodURL).
success(function (data, status, headers, config) {
deferred.resolve(data); // we get to here fine.
})......
In earlier version I could just do, in my controller:
$scope.theDates = myDates.getDates();
and the promises returned from getDates could be bound directly to a Select element.
Now this doesn't work and I'm forced to supply a callback on the promise in my controller or the data wont bind:
$scope.theDates = matchDates.getDates();
$scope.theDates.then(function (data) {
$scope.theDates = data; // this wasn't necessary in the past
The docs still say:
$q promises are recognized by the templating engine in angular, which means that in templates you can treat promises attached to a scope as if they were the resulting values.
They (promises) were working in older versions of Angular but in the 1.2 RC3 automatic binding fails in all my simple services.... any ideas on what I might be doing wrong.
There are changes in 1.2.0-rc3, including one you mentioned:
AngularJS 1.2.0-rc3 ferocious-twitch fixes a number of high priority
issues in $compile and $animate and paves the way for 1.2.
This release also introduces some important breaking changes that in some cases could break your directives and templates. Please
be sure to read the changelog to understand these changes and learn
how to migrate your code if needed.
For full details in this release, see the changelog.
There is description in change log:
$parse:
due to 5dc35b52, $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and
if absolutely needed, it can be reenabled during transitional period
via $parseProvider.unwrapPromises(true) api.
due to b6a37d11, feature added in rc.2 that unwraps return values from functions if the values are promises (if promise unwrapping is
enabled - see previous point), was reverted due to breaking a popular
usage pattern.
As #Nenad notices, promises are no longer automatically dereferenced. This is one of the most bizarre decisions I've ever seen since it silently removes a function that I relied on (and that was one of the unique selling points of angular for me, less is more). So it took me quite a bit of time to figure this out. Especially since the $resource framework still seems to work fine. On top of this all, this is also a release candidate. If they really had to deprecate this (the arguments sound very feeble) they could at least have given a grace period where there were warnings before they silently shut it off. Though usually very impressed with angular, this is a big minus. I would not be surprised if this actually will be reverted, though there seems to be relatively little outcry so far.
Anyway. What are the solutions?
Always use then(), and assign the $scope in the then method
function Ctrl($scope) {
foo().then( function(d) { $scope.d = d; });
)
call the value through an unwrap function. This function returns a field in the promise and sets this field through the then method. It will therefore be undefined as long as the promise is not resolved.
$rootScope.unwrap = function (v) {
if (v && v.then) {
var p = v;
if (!('$$v' in v)) {
p.$$v = undefined;
p.then(function(val) { p.$$v = val; });
}
v = v.$$v;
}
return v;
};
You can now call it:
Hello {{ unwrap(world) }}.
This is from http://plnkr.co/edit/Fn7z3g?p=preview which does not have a name associated with it.
Set $parseProvider.unwrapPromises(true) and live with the messages, which you could turn off with $parseProvider.logPromiseWarnings(false) but it is better to be aware that they might remove the functionality in a following release.
Sigh, 40 years Smalltalk had the become message that allowed you to switch object references. Promises as they could have been ...
UPDATE:
After changing my application I found a general pattern that worked quite well.
Assuming I need object 'x' and there is some way to get this object remotely. I will then first check a cache for 'x'. If there is an object, I return it. If no such object exists, I create an actual empty object. Unfortunately, this requires you to know if this is will be an Array or a hash/object. I put this object in the cache so future calls can use it. I then start the remote call and on the callback I copy the data obtained from the remote system in the created object. The cache ensures that repeated calls to the get method are not creating lots of remote calls for the same object.
function getX() {
var x = cache.get('x');
if ( x == undefined) {
cache.put('x', x={});
remote.getX().then( function(d) { angular.copy(d,x); } );
}
return x;
}
Yet another alternative is to provide the get method with the destination of the object:
function getX(scope,name) {
remote.getX().then( function(d) {
scope[name] = d;
} );
}
You could always create a Common angular service and put an unwrap method in there that sort of recreates how the old promises worked. Here is an example method:
var shared = angular.module("shared");
shared.service("Common", [
function () {
// [Unwrap] will return a value to the scope which is automatially updated. For example,
// you can pass the second argument an ng-resource call or promise, and when the result comes back
// it will update the first argument. You can also pass a function that returns an ng-resource or
// promise and it will extend the first argument to contain a new "load()" method which can make the
// call again. The first argument should either be an object (like {}) or an array (like []) based on
// the expected return value of the promise.
// Usage: $scope.reminders = Common.unwrap([], Reminders.query().$promise);
// Usage: $scope.reminders = Common.unwrap([], Reminders.query());
// Usage: $scope.reminders = Common.unwrap([], function() { return Reminders.query(); });
// Usage: $scope.reminders.load();
this.unwrap = function(result, func) {
if (!result || !func) return result;
var then = function(promise) {
//see if they sent a resource
if ('$promise' in promise) {
promise.$promise.then(update);
}
//see if they sent a promise directly
else if ('then' in promise) {
promise.then(update);
}
};
var update = function(data) {
if ($.isArray(result)) {
//clear result list
result.length = 0;
//populate result list with data
$.each(data, function(i, item) {
result.push(item);
});
} else {
//clear result object
for (var prop in result) {
if (prop !== 'load') delete result[prop];
}
//deep populate result object from data
$.extend(true, result, data);
}
};
//see if they sent a function that returns a promise, or a promise itself
if ($.isFunction(func)) {
// create load event for reuse
result.load = function() {
then(func());
};
result.load();
} else {
then(func);
}
return result;
};
}
]);
This basically works how the old promises did and auto-resolves. However, if the second argument is a function it has the added benefit of adding a ".load()" method which can reload the value into the scope.
angular.module('site').controller("homeController", function(Common) {
$scope.reminders = Common.unwrap([], Reminders.query().$promise);
$scope.reminders = Common.unwrap([], Reminders.query());
$scope.reminders = Common.unwrap([], function() { return Reminders.query(); });
function refresh() {
$scope.reminders.load();
}
});
These were some good answers, and helped me find my issue when I upgraded angular and my auto-unwrapping of promises stopped working.
At the risk of being redundant with Peter Kriens, I have found this pattern to work for me (this is a simple example of simply putting a number of famous people's quotes onto a page).
My Controller:
angular.module('myModuleName').controller('welcomeController',
function ($scope, myDataServiceUsingResourceOrHttp) {
myDataServiceUsingResourceOrHttp.getQuotes(3).then(function (quotes) { $scope.quotes = quotes; });
}
);
My Page:
...
<div class="main-content" ng-controller="welcomeController">
...
<div class="widget-main">
<div class="row" ng-repeat="quote in quotes">
<div class="col-xs-12">
<blockquote class="pull-right">
<p>{{quote.text}}</p>
<small>{{quote.source}}</small>
</blockquote>
</div>
</div>
</div>
...