Angularjs spinner control using $q.all - angularjs

I am implementing a spinner functionality in my project. The gold is to show the spinner when one or multiple http requests are fired, and hide the spinner when the requests are successful. Because I don't know which request will be resolved first, I chose to use $q.all. I have a directive like this:
angular.module('myApp', [])
.directive('spinner', function($q, $http) {
return {
controller: function($scope) {
$scope.showSpinner = true;
var self = this;
self.promises = [];
self.makeHeader = function() {
self.promises.push($http.get('some/url'));
// Code that builds header goes here.
};
self.makeFooter = function() {
self.promises.push($http.get('other/url'));
// Code that builds footer goes here.
};
self.makeHeader();
self.makeFooter();
// Initial page load
$q.all(self.promises).then(function() {
// Hide the spinner.
$scope.showSpinner = false;
});
}
}
});
The initial load works fine, but when user has an interaction which requires multiple calls to server, to rebuild the header and footer. How do I show the spinner again and hide it when the promises are resolved?

You can wrap the repeated calls into a function.
I also recommend to make the 2 functions to return promise instead of handling the $http promise inside to provide more flexibility.
angular.module('myApp', [])
.directive('spinner', function($q, $http) {
return {
controller: function($scope) {
var self = this;
self.makeHeader = function() {
return $http.get('some/url').then(function() {
// Code that builds header goes here.
});
};
self.makeFooter = function() {
return $http.get('other/url').then(function() {
// Code that builds footer goes here.
});
};
self.build = function() {
$scope.showSpinner = true;
self.promises = [];
self.promises.push(self.makeHeader());
self.promises.push(self.makeFooter());
$q.all(self.promises).then(function() {
// Hide the spinner.
$scope.showSpinner = false;
});
}
// initial run
self.build();
// subsequent call
self.someClickHandler = function() {
self.build();
}
// some other calls that need to use spinner
self.other = function() {
$scope.showSpinner = true;
self.promises = [];
self.promises.push(self.otherCall());
$q.all(self.promises).then(function() {
// Hide the spinner.
$scope.showSpinner = false;
});
}
}
}
});
As you can see this approach would look better if you always call same set of functions every time, but what if you need to use spinner on other cases like self.other?
You can wrap the spinner in a function and pass it the promise array.
var showSpinner = function(promises) {
$scope.showSpinner = true;
$q.all(promises).then(function() {
// Hide the spinner.
$scope.showSpinner = false;
});
}
self.build = function() {
var promises = [];
promises.push(self.makeHeader());
promises.push(self.makeFooter());
showSpinner(promises);
}
self.other = function() {
var promises = [];
promises.push(self.otherCall());
showSpinner(promises);
}
Looks cleaner?

Related

Can't get data out of service in angularjs

I'm using a service in order to pass data between different instances of an AngularJS controller. I know that this is not the best way to do it but it's the way that fits my case. The problem is that I cannot get data out of that Service.
var app = angular.module('MovieApp', ['ngResource']);
app.factory('factMovies', function($resource) { //this returns some movies from MongoDB
return $resource('/movies');
});
app.service('SnapshotService', function(factMovies) {
//this is used to pass data to different instances of the same controller
//omitted getters/setters
this.snapshots = [];
this.init = function() {
var ctrl = this;
var resp = factMovies.query({}, function() {
if (resp.error) {
console.log(resp.error)
} else {
tempDataset = []
//do stuff and put the results in tempDataset
ctrl.snapshots.push(tempDataset);
console.log(tempDataset); //prints fine
return tempDataset;
}
});
};
});
app.controller('TileController', function(SnapshotService) {
this.dataset = [];
this.filters = [];
this.init = function() {
var ctrl = this;
var data = SnapshotService.init(function() {
console.log(ctrl.data); //doesn't even get to the callback function
});
};
});
I really can't figure out what I'm doing wrong..
SnapshotService.init() doesn't take any parameters - meaning the anonymous function you pass in with the SnapshotService.init() call in TileController does nothing.
What you need to do is add the parameter to the init function definition and then call it in the code:
app.service('SnapshotService', function(factMovies) {
//this is used to pass data to different instances of the same controller
//omitted getters/setters
this.snapshots = [];
this.init = function(cb) {
var ctrl = this;
var resp = factMovies.query({}, function() {
if (resp.error) {
console.log(resp.error)
} else {
tempDataset = []
//do stuff and put the results in tempDataset
ctrl.snapshots.push(tempDataset);
console.log(tempDataset); //prints fine
cb(ctrl.snapshots);
}
});
};
});

$http.get causing infinites calls (and errors) until computer crashes

I want to swap an array and get a json file, but don't know why nor where there's something wrong in my code (the service/controller part without an http request works though).
incriminated code
(function() {
(function() {
var JsonsService;
JsonsService = function($http) {
var pizze;
pizze = [];
return {
getPizze: function() {
$http.get('data/pizze-it.json').then(function(pizze) {
pizze = pizze.data;
});
}
};
};
JsonsService.$inject = ['$http'];
angular.module('myApp').factory('JsonsService', JsonsService);
})();
}).call(this);
(function() {
(function() {
var JsonsCtrl;
JsonsCtrl = function(JsonsService) {
var self;
self = this;
self.list = function() {
return JsonsService.getPizze();
};
};
JsonsCtrl.$inject = ['JsonsService'];
angular.module('myApp').controller('JsonsCtrl', JsonsCtrl);
})();
}).call(this);
Plnkr
I removed from app.js the entire block of code that is causing this error (service and controller), and placed it inside DontLoadThis.js (there's some markup to put back into main.html too)
This isn't necessarily the definite answer but there's a few things I've noticed that appear wrong.
Starting with your JsonsService:
JsonsService = function($http) {
var pizze;
pizze = [];
return {
getPizze: function() {
$http.get('data/pizze-it.json').then(function(pizze) {
pizze = pizze.data;
});
}
};
};
You're initialising a variable pizze but also using the callback variable pizze in the $http.get(). Instead I suggest:
var pizze = [];
...
$http.get('data/pizze-it.json').then(function(json_response) {
pizze = json_response.data;
});
This however is made redundant by the second issue: JsonsService.getPizze() doesn't actually return anything. A possible way around this would be to return the promise from getPizze() and deal with the result in the controller.
// in service
return {
getPizze: function() {
return $http.get('data/pizze-it.json');
}
};
// in controller
JsonsCtrl = function(JsonsService) {
var self;
self = this;
self.list = [];
JsonsService.getPizze().then(function (json_response) {
self.list = json_response.data;
});
};

Calling a rest service only once in angularjs directive

We have menu's that can be displayed based on the role. I made the directive for this feature. But the problem is everytime I call the directive its calling rest service. I want to be able to call it only once and save the features that can be accessed for the current role and decide either hide it or show. Below is currently what I have:
var app = angular.module('myApp', []);
app.service('authService', function(){
var user = {};
user.role = 'guest';
return{
getFeature: function(){
//rest service call
return featureList;
},
}
});
app.directive('restrict', function(authService){
return{
restrict: 'A',
prioriry: 100000,
scope: false,
compile: function(element, attr, linker){
var accessDenied = true;
var featureList = authService.getFeature();
featureList.then(function(result){
featureList = result;
var attributes = attr.access.split(" ");
for(var i in featureList){
if(featureList[i] == attributes[0]){
accessDenied = false;
}
}
if(accessDenied){
element.children().remove();
element.remove();
}
});
}
}
});
Is there anyway I can call rest service only once not with every directive call?
One possibility is to cache the result from the service call, like so
app.service('authService', function($q) {
var user = {};
var cache;
user.role = 'guest';
return {
getFeature: function() {
if (cache) {
return $q.when(cache);
} else {
//rest service call
return featureList.then(function(val) {
cache = val;
return val;
});
}
}
}
});
You'll only make the call once (as long as the result is truthy), after that you'll just be returning the cache.
Might want to consider how to invalidate the cache at some point, depending on the apps needs.
Edit
As per comments, previous solution would allow multiple calls while waiting for the promise to resolve.
This approach will always return one singular promise, which itself does the single rest call.
app.service('authService', function($http) {
var user = {};
var prom;
user.role = 'guest';
return {
getFeature: function() {
if (!prom) {
prom = $http.get(/rest/).then(function(result) {
return result.data;
});
}
return prom;
}
}
});

How to integrate an AngularJS service with the digest loop

I'm trying to write an AngularJS library for Pusher (http://pusher.com) and have run into some problems with my understanding of the digest loop and how it works. I am writing what is essentially an Angular wrapper on top of the Pusher javascript library.
The problem I'm facing is that when a Pusher event is triggered and my app is subscribed to it, it receives the message but doesn't update the scope where the subscription was setup.
I have the following code at the moment:
angular.module('pusher-angular', [])
.provider('PusherService', function () {
var apiKey = '';
var initOptions = {};
this.setOptions = function (options) {
initOptions = options || initOptions;
return this;
};
this.setToken = function (token) {
apiKey = token || apiKey;
return this;
};
this.$get = ['$window',
function ($window) {
var pusher = new $window.Pusher(apiKey, initOptions);
return pusher;
}];
})
.factory('Pusher', ['$rootScope', '$q', 'PusherService', 'PusherEventsService',
function ($rootScope, $q, PusherService, PusherEventsService) {
var client = PusherService;
return {
subscribe: function (channelName) {
return client.subscribe(channelName);
}
}
}
]);
.controller('ItemListController', ['$scope', 'Pusher', function($scope, Pusher) {
$scope.items = [];
var channel = Pusher.subscribe('items')
channel.bind('new', function(item) {
console.log(item);
$scope.items.push(item);
})
}]);
and in another file that sets the app up:
angular.module('myApp', [
'pusher-angular'
]).
config(['PusherServiceProvider',
function(PusherServiceProvider) {
PusherServiceProvider
.setToken('API KEY')
.setOptions({});
}
]);
I've removed some of the code to make it more concise.
In the ItemListController the $scope.items variable doesn't update when a message is received from Pusher.
My question is how can I make it such that when a message is received from Pusher that it then triggers a digest such that the scope updates and the changes are reflected in the DOM?
Edit: I know that I can just wrap the subscribe callback in a $scope.$apply(), but I don't want to have to do that for every callback. Is there a way that I can integrate it with the service?
On the controller level:
Angular doesn't know about the channel.bind event, so you have to kick off the cycle yourself.
All you have to do is call $scope.$digest() after the $scope.items gets updated.
.controller('ItemListController', ['$scope', 'Pusher', function($scope, Pusher) {
$scope.items = [];
var channel = Pusher.subscribe('items')
channel.bind('new', function(item) {
console.log(item);
$scope.items.push(item);
$scope.$digest(); // <-- this should be all you need
})
Pusher Decorator Alternative:
.provider('PusherService', function () {
var apiKey = '';
var initOptions = {};
this.setOptions = function (options) {
initOptions = options || initOptions;
return this;
};
this.setToken = function (token) {
apiKey = token || apiKey;
return this;
};
this.$get = ['$window','$rootScope',
function ($window, $rootScope) {
var pusher = new $window.Pusher(apiKey, initOptions),
oldTrigger = pusher.trigger; // <-- save off the old pusher.trigger
pusher.trigger = function decoratedTrigger() {
// here we redefine the pusher.trigger to:
// 1. run the old trigger and save off the result
var result = oldTrigger.apply(pusher, arguments);
// 2. kick off the $digest cycle
$rootScope.$digest();
// 3. return the result from the the original pusher.trigger
return result;
};
return pusher;
}];
I found that I can do something like this and it works:
bind: function (eventName, callback) {
client.bind(eventName, function () {
callback.call(this, arguments[0]);
$rootScope.$apply();
});
},
channelBind: function (channelName, eventName, callback) {
var channel = client.channel(channelName);
channel.bind(eventName, function() {
callback.call(this, arguments[0]);
$rootScope.$apply();
})
},
I'm not really happy with this though, and it feels as though there must be something bigger than I'm missing that would make this better.

Angular: Rewriting function to use promise

I'm using an Angular factory that retrieves data from a feed and does some data manipulation on it.
I'd like to block my app from rendering the first view until this data preparation is done. My understanding is that I need to use promises for this, and then in a controller use .then to call functions that can be run as soon as the promise resolves.
From looking at examples I'm finding it very difficult to implement a promise in my factory. Specifically I'm not sure where to put the defers and resolves. Could anyone weigh in on what would be the best way to implement one?
Here is my working factory without promise:
angular.module('MyApp.DataHandler', []) // So Modular, much name
.factory('DataHandler', function ($rootScope, $state, StorageHandler) {
var obj = {
InitData : function() {
StorageHandler.defaultConfig = {clientName:'test_feed'};
StorageHandler.prepData = function(data) {
var i = 0;
var maps = StorageHandler.dataMap;
i = data.line_up.length;
while(i--) {
// Do loads of string manipulations here
}
return data;
}
// Check for localdata
if(typeof StorageHandler.handle('localdata.favorites') == 'undefined') {
StorageHandler.handle('localdata.favorites',[]);
}
},
};
return obj;
});
Here's what I tried from looking at examples:
angular.module('MyApp.DataHandler', []) // So Modular, much name
.factory('DataHandler', function ($rootScope, $q, $state, StorageHandler) {
var obj = {
InitData : function() {
var d = $q.defer(); // Set defer
StorageHandler.defaultConfig = {clientName:'test_feed'};
StorageHandler.prepData = function(data) {
var i = 0;
var maps = StorageHandler.dataMap;
i = data.line_up.length;
while(i--) {
// Do loads of string manipulations here
}
return data;
}
// Check for localdata
if(typeof StorageHandler.handle('localdata.favorites') == 'undefined') {
StorageHandler.handle('localdata.favorites',[]);
}
return d.promise; // Return promise
},
};
return obj;
});
But nothing is shown in console when I use this in my controller:
DataHandler.InitData()
.then(function () {
// Successful
console.log('success');
},
function () {
// failure
console.log('failure');
})
.then(function () {
// Like a Finally Clause
console.log('done');
});
Any thoughts?
Like Florian mentioned. Your asynchronous call is not obvious in the code you've shown.
Here is the gist of what you want:
angular.module("myApp",[]).factory("myFactory",function($http,$q){
return {
//$http.get returns a promise.
//which is latched onto and chained in the controller
initData: function(){
return $http.get("myurl").then(function(response){
var data = response.data;
//Do All your things...
return data;
},function(err){
//do stuff with the error..
return $q.reject(err);
//OR throw err;
//as mentioned below returning a new rejected promise is a slight anti-pattern,
//However, a practical use case could be that it would suppress logging,
//and allow specific throw/logging control where the service is implemented (controller)
});
}
}
}).controller("myCtrl",function(myFactory,$scope){
myFactory.initData().then(function(data){
$scope.myData = data;
},function(err){
//error loudly
$scope.error = err.message
})['finally'](function(){
//done.
});
});

Resources