AngularJS asynchronous function call - angularjs

How to call a function asynchronously inside an Angular controller? I'v read about promises, but this in not a http call so i can't use that.
Is there any solution to resolve these kind of problems?
I would like to display a spiner/loader until the called function finishing it's job.
//Controller JS
$scope.myFunctionCallOnClick = function()
{
if($scope.type=='TYPE1')
{
$scope.showSpiner();
//This function has called syncronously, and i can't display my spiner
Utils.doStuff(words,$scope);
$scope.hideSpiner();
}
else
{
Utils.doStuff2(words,$scope);
}
}
//Utils.js - this is my helper factory
angular.module('app').factory('Utils', function() {
var factory = {};
..
..
factory.doStuff = function()
{
var iteration = 10000000;
whilte(iteration > 0)
{
console.log(iteration);
}
}
..
..
return factory;
});

Use $timeout service to let the browser do some of its work (show your spinner in this case), and call doStuff from within the $timeout callback.
$scope.myFunctionCallOnClick = function () {
if ($scope.type == 'TYPE1') {
$scope.showSpiner();
$timeout(function () {
Utils.doStuff(words, $scope);
$scope.hideSpiner();
});
}
else {
Utils.doStuff2(words, $scope);
}
}

you can use $timeout to call a function asynchronously
$timeout(function(){
/// function logic
}, 0);
similarly you can even use.. $evalAsync
read more about them here

Related

Angular - unable to get a stream of data from $interval returned promise

I'm learning how to use angulars $interval by building a simple app that generates random string every second (inside a service) and returns it to the controller. However, I cant get the data. I am able to console.log it inside the function call, but not in the calling function inside the controller. What am I missing? I read here, but still dont get the point.
The controller:
angular.module("app").controller("authorC", function ($scope, WebService) {
$scope.generate = function () {
console.log("generating..");
WebService.generateRandom().then(function (y) {
console.log(y);
});
};
$scope.stop = function () {
WebService.stopGen();
};
});
The service:
angular.module("app").service("WebService", function ($http, $interval) {
function makeid() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 5; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
var genInterval;
this.generateRandom = function () {
genInterval = $interval(function () {
return makeid();
}, 1000);
return genInterval;
};
this.stopGen = function () {
$interval.cancel(genInterval);
console.log("Stopped");
}
});
$interval returns a promise that will get notified on each iteration, resolved when the timer finished to run (interval is completed) or rejected if the interval was cancelled.
https://docs.angularjs.org/api/ng/service/$interval
You can't use it to get a return value of the function that the interval runs. I'm not sure what you are trying to achieve, but you can for example save the randomValue on your service (and replace it on each iteration), so it will be able to access it from outside.

Best way to use Jasmine to test Angular Controller calls to Services with Promise return

After a week looking for a good answer/sample, I decided to post my question.
I need to know how is the best way to code and test something like this:
Controller
// my.controller.js
(function () {
'use strict';
angular.module('myApp.myModule').controller('Awesome', Awesome);
function Awesome($http, $state, AwesomeService) {
var vm = this; // using 'controllerAs' style
vm.init = init;
vm.awesomeThingToDo = awesomeThingToDo;
vm.init();
function awesomeThingToDo() {
AwesomeService.awesomeThingToDo().then(function (data) {
vm.awesomeMessage = data.awesomeMessage;
});
}
function init() {
vm.awesomeThingToDo(); // Should be ready on page start
}
}
})();
Service
// my.service.js
(function () {
'use strict';
angular.module('myApp.myModule').factory('AwesomeService', AwesomeService);
function AwesomeService($resource, $http) {
var service = {
awesomeThingToDo: awesomeThingToDo
}
return service;
function awesomeThingToDo() {
var promise = $http.get("/my-backend/api/awesome").then(function (response) {
return response.data;
});
return promise;
}
}
})();
My app works OK with this structure. And my Service unit tests are OK too.
But I don't know how to do unit tests on Controller.
I tried something like this:
Specs
// my.controller.spec.js
(function () {
'use strict';
describe("Awesome Controller Tests", function() {
beforeEach(module('myApp.myModule'));
var vm, awesomeServiceMock;
beforeEach(function () {
awesomeServiceMock = { Is this a good (or the best) way to mock the service?
awesomeThingToDo: function() {
return {
then: function() {}
}
}
};
});
beforeEach(inject(function ($controller) {
vm = $controller('Awesome', {AwesomeService : awesomeServiceMock});
}));
it("Should return an awesome message", function () {
// I don't know another way do to it... :(
spyOn(awesomeServiceMock, "awesomeThingToDo").and.callFake(function() {
return {
then: function() {
vm.awesomeMessage = 'It is awesome!'; // <-- I think I shouldn't do this.
}
}
});
vm.awesomeThingToDo(); // Call to real controller method which should call the mock service method.
expect(vm.awesomeMessage).toEqual('It is awesome!'); // It works. But ONLY because I wrote the vm.awesomeMessage above.
});
});
})();
My app uses Angular 1.2.28 and Jasmine 2.1.3 (with Grunt and Karma).
UPDATE: Solved!
it("Should return an awesome message", function () {
// Solved with callback parameter
spyOn(awesomeServiceMock, "awesomeThingToDo").and.callFake(function(callback) {
return {
then: function(callback) {
callback({awesomeMessage: 'It is awesome!'}); //callback call works fine! :D
}
}
});
I updated the question with a possible (bad) solution:
it("Should return an awesome message", function () {
// Solved with callback parameter
spyOn(awesomeServiceMock, "awesomeThingToDo").and.callFake(function(callback) {
return {
then: function(callback) {
callback({awesomeMessage: 'It is awesome!'}); //callback call works fine! :D
}
}
});
I used a callback to pass the mocked parameter and call the real implementation. :D
No, that's not how I would do this.
First, there is no need to create a mock service: you can inject the real one, and spy on it.
Second, Angular has everything you need to create promises and to resolve them. No need to create fake objects with a fake then() function.
Here's how I would do it:
describe("Awesome Controller Tests", function() {
beforeEach(module('myApp.myModule'));
var vm, awesomeService, $q, $rootScope;
beforeEach(inject(function($controller, _awesomeService_, _$q_, _$rootScope_) {
$q = _$q_;
awesomeService = _awesomeService_;
$rootScope = _$rootScope_;
vm = $controller('Awesome');
}));
it("Should return an awesome message", function () {
spyOn(awesomeService, "awesomeThingToDo").and.returnValue(
$q.when({
awesomeMessage: 'awesome message'
}));
vm.awesomeThingToDo();
// at this time, the then() callback hasn't been called yet:
// it's called at the next digest loop, that we will trigger
$rootScope.$apply();
// now the then() callback should have been called and initialized
// the message in the controller with the message of the promise
// returned by the service
expect(vm.awesomeMessage).toBe('awesome message');
});
});
Unrelated note: 1.2.28 is quite old. You should migrate to the latest version.

Using an Angular Service for async HttpRequests - how to callback?

I might be totally confused on how to properly use callback methods for ajax calls in angular. I have the following angular app and cannot figure out how to display the testUser object only after the ajax call is successfully completed.
I have an ng controller like so:
Controllers.controller('mainController', ['$scope','UserService', function ($scope, UserService) {
...
$scope.testUser = UserService.getTestUser();
...
}
The UserService is defined like so:
Services.service('UserService', function () {
...
this.getTestUser = function() {
...
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if(xmlhttp.readyState == 4 && xmlhttp.status == 200) {
return JSON.parse(xmlhttp.responseText);
}
};
xmlhttp.open('GET',url,false); //async set to false
xmlhttp.send();
}
...
}
Currently, the $scope.testUser is 'undefined' and blank on the page because it is being displayed before the ajax call completes. You can see in my service function that I have async set to false, but it doesnt seem to matter.
I've confirmed the ajax call does eventually return a populated user object. What am I missing to make the page display $scope.testUser only when its been successfully retrieved?
Thanks to Slava and georgeawg. I changed to the following and everything works great!
Controllers.controller('mainController', ['$scope','UserService', function ($scope, UserService) {
...
UserService.getTestUser.async().then(function(testUser) {
$scope.testUser = testUser;
};
...
}
And on the service side I have this:
Services.service('UserService', function ($http) {
...
this.getTestUser = {
async: function() {
var promise = $http.get(url).then(function(response) {
return response.data;
};
return promise;
}
}
Thank you!

AngularJS value won't change with setTimeout

I wanted to change a scope variable after the page has been initialized.
I have a angular application with following code:
$scope.names = ['Jack'];
append_name = function(){$scope.names.push('Bob');}
setTimeout(append_name, 2000);
Tough I don't see the value change after the specified delay.
Here is the plunker http://plnkr.co/edit/FBa2fwb7js8pRNENNJof
Short answer: use the built-in $timeout service instead of setTimeout:
http://plnkr.co/edit/nh1jEhocRpXtD0rUTh4k?p=preview
Long answer is here: https://stackoverflow.com/a/9693933/1418796
If you create code outside of angular you need to tell that you change something with $apply
$scope.names = ['Jack'];
append_name = function() {
$scope.$apply(function() {
$scope.names.push('Bob');
});
};
setTimeout(append_name, 2000);
You can create handy higher order function to wrap your functions with $apply:
function ngWrap($scope, fn) {
return function() {
var args = [].slice.call(arguments);
if ($scope.$$phase) {
fn.apply(null, args);
} else {
return $scope.$apply(function() {
fn.apply(null, args);
});
}
};
}
this can be used like:
setTimeout(ngWrap($scope, function() {
$scope.names.push('Bob');
}), 2000);
Also angular have $timeout that will handle this.

ngProgress loading bar on every page load

I am using AngularJS and ngProgress to display a YouTube-like loading bar at the top of my site.
The bar is started, then new data is loaded in via ajax, and once the request is finished, the bar is completed.
Example:
var TestCtrl = function( $scope, $location, Tests, ngProgress )
{
// start progressbar
ngProgress.start();
$scope.tests = Tests.query(
function()
{
// end progressbar
ngProgress.complete()
}
);
};
Now my question is: How can I integrate this principle higher up in the order of things, such that I don't have to repeat the code for every single controller?
You could use a service which controls ngProgress (acting like a wrapper over it) and listen for changes in the url.
Each time the url changes the event $locationChangeSuccess is broadcasted (more info at $location) which we could listen to invoke ngProgress.start()
However we don't know when it's completed (we can't have a bar on the top loading forever), therefore we need to call ngProgress.complete() explicitly in our controllers OR we could assume that our async functions might take like 5 seconds to be completed and call ngProgress.complete() using a timer in our wrapper service
When the loading bar is already visible and there's a change in the url we need to reset the status of the bar by calling ngProgress.reset()
You can use the following approach to solve these problems:
angular.module('myApp').factory('Progress', function (ngProgress) {
var timer;
return {
start: function () {
var me = this;
// reset the status of the progress bar
me.reset();
// if the `complete` method is not called
// complete the progress of the bar after 5 seconds
timer = setTimeout(function () {
me.complete();
}, 5000);
},
complete: function () {
ngProgress.complete();
if (timer) {
// remove the 5 second timer
clearTimeout(timer);
timer = null;
}
},
reset: function () {
if (timer) {
// remove the 5 second timer
clearTimeout(timer);
// reset the progress bar
ngProgress.reset();
}
// start the progress bar
ngProgress.start();
}
};
});
To listen for changes in the url and show the progress bar we could use:
angular.module('myApp')
.run(function (Progress) {
$rootScope.$on('$locationChangeSuccess', function () {
Progress.start();
});
}
Now we can manually control the completeness of the status bar by injecting the Progress service and calling the method Progress.complete() when all of our async functions have finished (we could also control this from any service that makes async calls):
angular.module('myApp')
.controller('SomeCtrl', function (Progress) {
setTimeout(function () {
Progress.complete();
}, 2000);
});
Here is an example using Interceptor:
.factory('interceptorNgProgress', [
'ngProgressFactory', function (ngProgressFactory) {
var complete_progress, getNgProgress, ng_progress, working;
ng_progress = null;
working = false;
getNgProgress = function() {
if(!ng_progress) {
ng_progress = ngProgressFactory.createInstance();
return ng_progress;
}
return ng_progress;
};
complete_progress = function() {
var ngProgress;
if (working) {
ngProgress = getNgProgress();
ngProgress.complete();
return working = false;
}
};
return {
request: function(request) {
var ngProgress;
ngProgress = getNgProgress();
if (request.url.indexOf('.html') > 0) {
return request;
}
if (!working) {
ngProgress.reset();
ngProgress.start();
working = true;
}
return request;
},
requestError: function(request) {
complete_progress();
return request;
},
response: function(response) {
complete_progress();
return response;
},
responseError: function(response) {
complete_progress();
return response;
}
}
}])
.config(function ($httpProvider) {
$httpProvider.interceptors.push('interceptorNgProgress');
});
I would put it in an angular directive and then you can pass that into any controller you want to be able to use it.
http://docs.angularjs.org/guide/directive
Edit, thinking about it a service mght be better for this case.
http://docs.angularjs.org/guide/dev_guide.services.creating_services
You apparently already have a Tests service. Override it so ngProgress is injected into it and have Tests always call ngProgress.start() and ngProgress.complete() for you in query.

Resources