ngProgress loading bar on every page load - angularjs

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.

Related

RxJS in AngularJS Services

I'm learning RxJS and am trying to implement it in one of my existing applications. First thing I am trying to do is remove rootscope.broadcast/emit methods and replace them with BehaviorSubjects. This has worked fine if those events are subscribed to inside a controller. However, if I try to subscribe to those events in a service, they never fire. I can move the exact same subscription into a component/controller/etc and it works fine.
Is there a reason for this or should I be able to do this and I am just doing something wrong?
UPDATE 1
I have an event service that maintains events run at the top of the application like so:
(function () {
"use strict";
angular.module("app.common").service("eventService", EventService);
function EventService($window) {
"ngInject";
var defaultBehaviorSubjectValue = null;
var service = {
activate: activate,
onLogin: new Rx.BehaviorSubject(defaultBehaviorSubjectValue), //onnext by authService.login
onLogout: new Rx.BehaviorSubject(defaultBehaviorSubjectValue), //onnext by authService.logout
isOnline: new Rx.BehaviorSubject(defaultBehaviorSubjectValue) //onnext by authService.logout
};
return service;
function activate() {
service.onLogin.subscribe(function (userData) {
console.info("user logged in");
});
service.onLogout.subscribe(function (userData) {
console.info("Logging user out");
dispose();
});
$window.addEventListener("offline", function () {
console.info("Lost internet connection, going offline");
service.isOnline.onNext(false);
}, false);
$window.addEventListener("online", function () {
console.info("Regained internet connection, going online");
service.isOnline.onNext(true);
}, false);
}
function dispose() {
angular.forEach(service, function (event, index) {
if (service && service[event] && service[event].isDisposed === false) {
service[event]();
}
});
}
}
})();
I have a service that is waiting for that onLogin event to fire. The code is simliar to:
(function () {
angular.module("app.data").service("offlineProjectDataService", OfflineProjectDataService);
function OfflineProjectDataService(definitions, metaDataService, userService, eventService) {
"ngInject";
var onLoginSubscription = eventService.onLogin.subscribe(function (isLoaded) {
if (isLoaded) {
activate();
}
});
var service = {
activate: activate
}
return service;
function activate(){
//..some stuff
}
}
})();
The problem I'm having is that the onLogin event is not firing so my activate method is not being called. If I move that exact same subscription line to some other controller in my app, it DOES fire. So I don't think there is anything wrong with syntax.
Unless of course I'm missing something here that is probably painfully obvious to somebody.
Use RxJS in a Service
Build a service with RxJS Extensions for Angular.
<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/rx/dist/rx.all.js"></script>
<script src="//unpkg.com/rx-angular/dist/rx.angular.js"></script>
var app = angular.module('myApp', ['rx']);
app.factory("DataService", function(rx) {
var subject = new rx.Subject();
var data = "Initial";
return {
set: function set(d){
data = d;
subject.onNext(d);
},
get: function get() {
return data;
},
subscribe: function (o) {
return subject.subscribe(o);
}
};
});
Then simply subscribe to the changes.
app.controller('displayCtrl', function(DataService) {
var $ctrl = this;
$ctrl.data = DataService.get();
var subscription = DataService.subscribe(function onNext(d) {
$ctrl.data = d;
});
this.$onDestroy = function() {
subscription.dispose();
};
});
Clients can subscribe to changes with DataService.subscribe and producers can push changes with DataService.set.
The DEMO on PLNKR.

AngularJS asynchronous function call

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

AngularJS call scope function to 'refresh' scope model

I've been struggling with this for a few days now and can't seem to find a solution.
I have a simple listing in my view, fetched from MongoDB and I want it to refresh whenever I call the delete or update function.
Although it seems simple that I should be able to call a previously declared function within the same scope, it just doesn't work.
I tried setting the getDispositivos on a third service, but then the Injection gets all messed up. Declaring the function simply as var function () {...} but it doesn't work as well.
Any help is appreciated.
Here's my code:
var myApp = angular.module('appDispositivos', []);
/* My service */
myApp.service('dispositivosService',
['$http',
function($http) {
//...
this.getDispositivos = function(response) {
$http.get('http://localhost:3000/dispositivos').then(response);
}
//...
}
]
);
myApp.controller('dispositivoController',
['$scope', 'dispositivosService',
function($scope, dispositivosService) {
//This fetches data from Mongo...
$scope.getDispositivos = function () {
dispositivosService.getDispositivos(function(response) {
$scope.dispositivos = response.data;
});
};
//... and on page load it fills in the list
$scope.getDispositivos();
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo);
$scope.getDispositivos(); //it should reload the view here...
$scope.dispositivo = '';
};
$scope.removeDispositivo = function (id) {
dispositivosService.removerDispositivo(id);
$scope.getDispositivos(); //... here
};
$scope.editDispositivo = function (id) {
dispositivosService.editDispositivo(id);
$scope.getDispositivos(); //... and here.
};
}
]
);
On service
this.getDispositivos = function(response) {
return $http.get('http://localhost:3000/dispositivos');
}
on controller
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo).then(function(){
$scope.getDispositivos(); //it should reload the view here...
$scope.dispositivo = '';
});
};
None of the solutions worked. Later on I found that the GET request does execute, asynchronously however. This means that it loads the data into $scope before the POST request has finished, thus not including the just-included new data.
The solution is to synchronize the tasks (somewhat like in multithread programming), using the $q module, and to work with deferred objects and promises. So, on my service
.factory('dispositivosService',
['$http', '$q',
function($http, $q) {
return {
getDispositivos: function (id) {
getDef = $q.defer();
$http.get('http://myUrlAddress'+id)
.success(function(response){
getDef.resolve(response);
})
.error(function () {
getDef.reject('Failed GET request');
});
return getDef.promise;
}
}
}
}
])
On my controller:
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo)
.then(function(){
dispositivosService.getDispositivos()
.then(function(dispositivos){
$scope.dispositivos = dispositivos;
$scope.dispositivo = '';
})
});
};
Being my 'response' object a $q.defer type object, then I can tell Angular that the response is asynchronous, and .then(---).then(---); logic completes the tasks, as the asynchronous requests finish.

Error: Timeout - Async callback was not invoked within timeout specified.... .DEFAULT_TIMEOUT_INTERVAL

I have an angular service class : -
angular.module('triggerTips')
.service('userData', function ($rootScope, $http, $log, $firebase) {
this._log = {
service : 'userData'
};
// Synchronized objects storing the user data
var config;
var userState;
// Loads the user data from firebase
this.init = function(readyCallback) {
var log = angular.extend({}, this._log);
log.funct = 'init';
var fireRef = new Firebase('https://XYZfirebaseio.com/' + $rootScope.clientName);
config = $firebase(fireRef.child('config')).$asObject();
userState = $firebase(fireRef.child('userState').child($rootScope.userName)).$asObject();
Promise.all([config.$loaded(), userState.$loaded()]).
then(
function() {
if(config == null || Object.keys(config).length < 4) {
log.message = 'Invalid config';
$log.error(log);
return;
}
if(!userState.userProperties) {
userState.userProperties = {};
}
if(!userState.contentProperties) {
userState.contentProperties = {};
}
log.message = 'User Properties: ' + JSON.stringify(userState.userProperties);
$log.debug(log);
log.message = 'Content Properties: ' + JSON.stringify(userState.contentProperties);
$log.debug(log);
log.message = 'Loaded user data from firebase';
$log.debug(log);
readyCallback();
},
function() {
log.message = 'Unable to load user data from firebase';
$log.error(log);
}
);
};
// Returns the initial tip configuration
this.getConfig = function() {
return config;
};
// Set the value of a user property
// A user property is something about the user himself
this.setUserProperty = function(property, value) {
if(!userState.userProperties) {
userState.userProperties = {};
}
userState.userProperties[property] = value;
userState.$save();
$rootScope.$broadcast('user-property-change', property);
};
// Get the value of a user property
this.getUserProperty = function(property) {
if(userState.userProperties) {
return userState.userProperties[property];
}
};
// Set the value of a user content property
// A content property is something about a particular peice of content for a particular user
this.setContentProperty = function(contentName, property, value) {
if(!userState.contentProperties[contentName]) {
userState.contentProperties[contentName] = {};
}
userState.contentProperties[contentName][property] = value;
userState.$save();
$rootScope.$broadcast('content-property-change', contentName, property);
};
// Increment a count property on the user state for a given tip
this.incrementContentProperty = function(contentName, property) {
if(!userState.contentProperties[contentName]) {
userState.contentProperties[contentName] = {};
}
if(!userState.contentProperties[contentName][property]) {
userState.contentProperties[contentName][property] = 0;
}
userState.contentProperties[contentName][property]++;
userState.$save();
$rootScope.$broadcast('content-property-change', contentName, property);
};
// Returns the user state for a given tip and property
this.getContentProperty = function(contentName, property) {
if(userState.contentProperties) {
var t = userState.contentProperties[contentName];
if(t) {
return t[property];
}
}
};
});
I am trying to unit test this service using jasmine:-
my unit test is :-
'use strict';
describe('Service: userData', function () {
// load the service's module
beforeEach(function() {
module('triggerTips');
});
// instantiate service
var userData;
beforeEach(inject(function (_userData_) {
userData = _userData_;
}));
it('should load correctly', function () {
expect(!!userData).toBe(true);
});
describe('after being initialized', function () {
beforeEach(function(done) {
// Unable to get this working because the callback is never called
userData.init(function() {
done();
});
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;
});
it('should have a valid config', function (done) {
setTimeout(function() {
expect(Object.keys(userData.getConfig()).length == 0);
done();
}, 1500);}); }); });
I read about the Asynchronous Support in Jasmine, but as I am rather new to unit testing with JavaScript couldn't make it work.
I am receiving an error :
Async callback was not invoked within timeout specified by
jasmine.DEFAULT_TIMEOUT_INTERVAL
Can somebody help me providing working example of my code with some explanation?
I would suggest that you replace setTimeout with $timeout so as to speed up your spec suite. You will need ngMock to be a part of your spec suite in order to get this working the intended way, but that seems to have already been taken care of looking at your spec. Good stuff.
Then in order to make the async nature of the spec "go away" you would call:
$timeout.flush([delay]) where delay is optional.
If no delay is passed, all the pending async tasks (inside the angular world) will finish what they're doing.
If a delay is passed, all pending tasks within the specified delay will finish. Those outside of the specified delay will remain in a 'pending' state.
With this, you can remove the done callback and write your tests as such:
describe('after being initialized', function () {
var $timeout;
beforeEach(function () {
// Unable to get this working because the callback is never called
userData.init();
inject(function ($injector) {
$timeout = $injector.get('$timeout');
});
}));
it('should have a valid config', function () {
$timeout.flush();
// callback should've been called now that we flushed().
expect(Object.keys(userData.getConfig()).length).toEqual(0);
});
});
What Promise implementation are you using? I see a call to Promise.all but for the sake of continuing on with my answer I'm going to assume it is equivalent to $q.all. Running $timeout.flush should take care of resolving those values.
If you want to write expectations on the rejected/resolved values of a promise in Jasmine, I would look into something like jasmine-promise-matchers to make it clean and pretty, but barring that you could do something like this:
// $q
function get () {
var p1 = $timeout(function () { return 'x'; }, 250);
var p2 = $timeout(function () { return 'y'; }, 2500);
return $q.all([p1, p2]);
}
// expectation
it('is correct', function () {
var res;
get().then(function (r) {
res = r;
});
$timeout.flush(2500);
expect(res).toEqual(['x', 'y']);
});
Depending on your setup, you may or may not have to stub out/spy on (depending on your frameworks definition of a spy) the promise in relation to your local config variable, but that's another story altogether I reckon.
I am not at all familiar with $firebase(something).$asObject.$loaded - as such I may have missed something here, but assuming it works 'just like any other promise' you should be good to go.
jsfiddle

How to display loading image, then make it disappear after 4 ajax requests in angularjs?

Assuming I have a div that I want to show when the page first loads showing the "number of ajax requests not complete" / "number of ajax requests completed", then make it disappear after X number of ajax requests are completed, (X can be a number set in javascript). Would like to see examples on how this would work. So far I only know that you can "emit" a "LOADING" and "DONELOADING" event for the show/hide of the one div, though that only works with a single http request.
Can use a promise array and $q.all() to determine when all requests are done.
Simple example since no code was provided
var promises=[];
for(i=0; i<5; i++){
var request=$http.get('someFile');
promises.push(request};
}
/* inject $q as dependency wherever you use this*/
$q.all(promises).then(function(){
/* remove loader*/
});
Don't necessarily have to use events to change loader visibility, could use ng-show and change the model property you assign to ng-show within $q.all(). For more detailed approach would need to see some code examples from your app
angular $q docs
To count the request you should use the requestInterceptor api of angulars $http service.
var module = angular.module("app", []);
module.factory("RequestStatistic", function () {
var requests = 0,
responses = 0,
incrementRequest = function () {
requests++;
},
incrementResponse = function () {
responses++;
},
getTotalRequests = function () {
return requests;
},
getTotalResponses = function () {
return responses;
},
getPendingRequests = function () {
return requests - responses;
};
return {
incrementRequest: incrementRequest,
incrementResponse: incrementResponse,
getTotalRequests: getTotalRequests,
getTotalResponses: getTotalResponses,
getPendingRequests: getPendingRequests
};
});
module.factory("RequestStatisticInterceptor", function ($q, RequestStatistic) {
return {
request: function (config) {
RequestStatistic.incrementRequest();
return config || $q.when(config);
},
response: function (response) {
RequestStatistic.incrementResponse();
return response || $q.when(response);
},
responseError: function (rejection) {
RequestStatistic.incrementResponse();
return $q.reject(rejection);
}
};
});
module.config(function($httpProvider){
$httpProvider.interceptors.push('RequestStatisticInterceptor');
});
Now you could make a directive that wathces the RequestStatistic Service and reacts appropriattly
Not exactly sure of what you mean with "ajax request", but i'm assuming you're calling some sort of asynchronous service for example:
angular.module('myApp').factory('myService', [
'$q',
'$timeout',
function($q, $timeout){
return {
load: function(millisecs) {
var deferred = $q.defer();
$timeout(function(){
deferred.resolve({foo: 'bar'});
}, (Math.floor(Math.random() * 9) + 1) * 1000);
return deferred.promise;
}
}
}
]);
You could keep a counter in your controller and increment it when each request completes:
angular.module('myApp').controller('myController', [
'$scope',
'$timeout',
'myService',
function($scope, myService){
$scope.requests = {
count: 10,
started: 0,
completed: 0
}
while($scope.requests.started < $scope.requests.count){
$scope.requests.started++
myService.load().then(function(){
$scope.requests.completed++
});
}
}
]);
Example # JSFiddle: http://jsfiddle.net/iH473/3rGjm/

Resources