Hide form while loading data - angularjs

In my AngularJS app I want to hide some page elements (sometimes all of them) until all async data is loaded.
Any suggestions on how to solve that when I have several data requests in the same controller?
I also would like to combine it with http://victorbjelkholm.github.io/ngProgress and find a solution with a service that listens to the status of dataLoaded (or whatever solution is best) that I can use in all different controllers of the app.
controller.js
$scope.dataLoaded = false;
dataFactory.getProduct(accessToken, storeId).then(function (response) {
$scope.formData = response.data;
$scope.dataLoaded = true;
}, function (error) {
console.log('Error: dataFactory.getProduct');
});
dataFactory.getBrand().then(function (response) {
$scope.brandData = response.data;
$scope.dataLoaded = true;
}, function (error) {
console.log('Error: dataFactory.getBrand');
});
service.js
app.factory("dataFactory", function ($http) {
var factory = {};
factory.getProducts = function (accessToken, storeId) {
return $http.get('data/getProducts.aspx?accessToken=' + accessToken + '&storeId=' + storeId)
};
factory.getProduct = function (accessToken, storeId, productId) {
return $http.get('data/getProduct.aspx?accessToken=' + accessToken + '&storeId=' + storeId + '&productId=' + productId)
};
factory.getBrand = function (accessToken, storeId) {
return $http.get('data/getBrand.aspx?accessToken=' + accessToken + '&storeId=' + storeId)
};
return factory;
});

what you can do is to resolve that function using your route provider with a combination of ng-cloak class that works like a charm, and also you can hide and show a "loader image" while you are switching routes.
let's say you have this service in your app:
app.factory("greetingService", function($q, $timeout){
return {
getGreeting: function(){
var deferred = $q.defer();
$timeout(function(){
deferred.resolve("Allo!");
},2000);
return deferred.promise;
}
}
});
setting up the "resolve" function
.when("/anyroute", {
templateUrl: "anyview.html",
controller: "anyController",
resolve: {
greeting: function(greetingService){
return greetingService.getGreeting();
}
}
})
in your controller:
app.controller("anyController", function ($scope, greeting) {
$scope.greeting = greeting;
});
you can inject your "resolved" dependency right before your controller, that way you'll have the initial data when the controller loads.
for showing and hiding a loader you can do a directive like
angular.module('app').directive('loaderBar', function ($rootScope) {
return {
restrict: 'EA',
replace: true,
template: '<div class="showloader"><img src="loader.gif" alt="loading..." /></div>',
link: function (scope, element) {
var namedListener = $rootScope.$$listeners['$stateChangeStart'];
if (namedListener === null || namedListener === undefined) {
$rootScope.$on('$stateChangeStart', function () {
element.addClass('animatedLoader');
});
$rootScope.$on('$stateChangeSuccess', function () {
element.removeClass('animatedLoader');
});
}
}
};
});
and don't forget to use ng-cloak class to hide the contents while loading at the top of each view, that will work like a charm!

Related

access the data from the resolve function without relading the controller

How do we access the data from the resolve function without relading the controller?
We are currently working on a project which uses angular-ui-router.
We have two seperated views: on the left a list of parent elements, on the right that elements child data.
If selecting a parent on the left, we resolve it's child data to the child-view on the right.
With the goal not to reaload the childs controller (and view), when selecting a different parent element, we set notify:false.
We managed to 're-resolve' the child controllers data while not reloading the controller and view, but the data (scope) won't refresh.
We did a small plunker to demonstrate our problem here
First click on a number to instantiate the controllers childCtrl. Every following click should change the child scopes data - which does not work.
You might notice the alert output already has the refreshed data we want to display.
Based on sielakos answer using an special service i came up with this solution.
First, i need a additional service which keeps a reference of the data from the resovle.
Service
.service('dataLink', function () {
var storage = null;
function setData(data) {
storage = data;
}
function getData() {
return storage;
}
return {
setData: setData,
getData: getData
};
})
Well, i have to use the service in my resolve function like so
Resolve function
resolve: {
detailResolver: function($http, $stateParams, dataLink) {
return $http.get('file' + $stateParams.id + '.json')
.then(function(response) {
alert('response ' + response.data.id);
dataLink.setData(response.data);
return response.data;
});
}
}
Notice the line dataLink.setData(response.data);. It keeps the data from the resolve in the service so I can access it from within the controller.
Controller
I modified the controller a little. I wrapped all the initialisation suff in an function i can execute when the data changes.
The second thing is to watch the return value of the dataLink.getData();
As of https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch $scope.$watch provides functionality to watch return values of functions.
Here is some Q&D example:
.controller('childCtrl', function($scope, $log, detailResolver, $interval, dataLink) {
initialise();
/*
* some stuff happens here
*/
$interval(function() {
console.log(detailResolver.id)
}, 1000);
$scope.$watch(dataLink.getData, function(newData) {
detailResolver = newData;
initialise();
});
function initialise() {
$log.info('childCtrl detailResolver.id == ' + detailResolver);
$scope.id = detailResolver;
}
})
The line $scope.$watch(dataLink.getData, function(newData) { ... }); does the trick. Every time the data in the dataLink service changes the callback kicks in and replaces the old data with the new one.
Ive created a plunker so you can give it a try https://plnkr.co/edit/xyZKQgENrwd4uEwS9QIM
You don't have to be afraid of memory leaks using this solution cause angular is removing watchers automatically. See https://stackoverflow.com/a/25114028/6460149 for more information.
Not so pretty, but working solution would be to use events. Well, maybe it is not that bad, at least it is not complicated.
https://plnkr.co/edit/SNRFhaudhsWLKUNMFos6?p=preview
angular.module('app',[
'ui.router'
])
.config(function($stateProvider) {
$stateProvider.state('parent', {
views:{
'parent':{
controller: 'parentCtrl',
template: '<div id="parent">'+
'<button ng-click="go(1)">1</button><br>'+
'<button ng-click="go(2)">2</button><br>'+
'<button ng-click="go(3)">3</button><br>'+
'</div>'
},
},
url: ''
});
$stateProvider.state('parent.child', {
views:{
'child#':{
controller: 'childCtrl',
template:'<b>{{ id }}</b>'
}
},
url: '/:id/child',
resolve: {
detailResolver: function($http, $stateParams, $rootScope) {
return $http.get('file'+$stateParams.id+'.json')
.then(function(response) {
alert('response ' + response.data.id);
$rootScope.$broadcast('newData', response.data);
return response.data;
});
}
}
});
})
.controller('parentCtrl', function ($log, $scope, $state) {
$log.info('parentCtrl');
var notify = true;
$scope.go = function (id) {
$state.go('parent.child', {id: id}, {notify:notify});
notify = false;
};
})
.controller('childCtrl', function ($scope, $log, detailResolver, $interval) {
/*
* some stuff happens here
*/
$log.info('childCtrl detailResolver.id == ' + detailResolver);
$scope.$on('newData', function (event, detailResolver) {
$scope.id = detailResolver;
});
$scope.id = detailResolver;
$interval(function(){
console.log(detailResolver.id)
},1000)
})
;
EDIT:
A little bit more complicated solution, that requires changing promise creator function into observables, but works:
https://plnkr.co/edit/1j1BCGvUXjtv3WhYN84T?p=preview
angular.module('app', [
'ui.router'
])
.config(function($stateProvider) {
$stateProvider.state('parent', {
views: {
'parent': {
controller: 'parentCtrl',
template: '<div id="parent">' +
'<button ng-click="go(1)">1</button><br>' +
'<button ng-click="go(2)">2</button><br>' +
'<button ng-click="go(3)">3</button><br>' +
'</div>'
},
},
url: ''
});
$stateProvider.state('parent.child', {
views: {
'child#': {
controller: 'childCtrl',
template: '<b>{{ id }}</b>'
}
},
url: '/:id/child',
resolve: {
detailResolver: turnToObservable(['$http', '$stateParams', function($http, $stateParams) { //Have to be decorated either be this or $inject
return $http.get('file' + $stateParams.id + '.json')
.then(function(response) {
alert('response ' + response.data.id);
return response.data;
});
}])
}
});
})
.controller('parentCtrl', function($log, $scope, $state) {
$log.info('parentCtrl');
var notify = true;
$scope.go = function(id) {
$state.go('parent.child', {id: id}, {notify: notify});
notify = false;
};
})
.controller('childCtrl', function($scope, $log, detailResolver, $interval) {
/*
* some stuff happens here
*/
$log.info('childCtrl detailResolver.id == ' + detailResolver);
detailResolver.addListener(function (id) {
$scope.id = id;
});
});
function turnToObservable(promiseMaker) {
var promiseFn = extractPromiseFn(promiseMaker);
var listeners = [];
function addListener(listener) {
listeners.push(listener);
return function() {
listeners = listeners.filter(function(other) {
other !== listener;
});
}
}
function fireListeners(result) {
listeners.forEach(function(listener) {
listener(result);
});
}
function createObservable() {
promiseFn.apply(null, arguments).then(fireListeners);
return {
addListener: addListener
};
}
createObservable.$inject = promiseFn.$inject;
return createObservable;
}
function extractPromiseFn(promiseMaker) {
if (angular.isFunction(promiseMaker)) {
return promiseMaker;
}
if (angular.isArray(promiseMaker)) {
var promiseFn = promiseMaker[promiseMaker.length - 1];
promiseFn.$inject = promiseMaker.slice(0, promiseMaker.length - 1);
return promiseFn;
}
}
1) For current task ng-view is not needed (IMHO). If you need two different scopes then redesign ng-views to become directives with their own controllers. This will prevent angular to reload them
2) if you need to share data between scopes then service could be used to store data (see helperService in the following code)
3) if we talk about current code simplification then it could be done so: use service from 2) and just use one controller:
(function() {
angular.module('app',[
'ui.router'
]);
})();
(function() {
angular
.module('app')
.service('helperService', helperService);
helperService.$inject = ['$http', '$log'];
function helperService($http, $log) {
var vm = this;
$log.info('helperService');
vm.data = {
id: 0
};
vm.id = 0;
vm.loadData = loadData;
function loadData(id) {
vm.id = id;
$http
.get('file'+id+'.json')
.then(function(response) {
alert('response ' + response.data.id);
vm.data = response.data;
});
}
}
})();
(function() {
angular
.module('app')
.controller('AppController', ParentController);
ParentController.$inject = ['helperService', '$log'];
function ParentController(helperService, $log) {
var vm = this;
$log.info('AppController');
vm.helper = helperService;
}
})();
4) interval, watch, broadcast, etc are not needed as well
Full code is here: plunker
P.S. don't forget about angularjs-best-practices/style-guide

AngularJS Factory object not being updated on controller and view

I'm with a problem with binding an object of a Factory and a Controller and it's view.
I am trying to get the fileUri of a picture selected by the user. So far so good. The problem is that I am saving the value that file to overlays.dataUrl. But I am referencing it on the view and it isn't updated. (I checked and the value is actually saved to the overlays.dataUrl variable.
Here goes the source code of settings.service.js:
(function () {
"use strict";
angular
.module("cameraApp.core")
.factory("settingsService", settingsService);
settingsService.$inject = ["$rootScope", "$cordovaFileTransfer", "$cordovaCamera"];
function settingsService($rootScope, $cordovaFileTransfer, $cordovaCamera) {
var overlays = {
dataUrl: "",
options: {
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
destinationType: Camera.DestinationType.FILE_URI
}
};
var errorMessages = [];
var service = {
overlays: overlays,
selectOverlayFile: selectOverlayFile,
errorMessages: errorMessages
};
return service;
function selectOverlayFile() {
$cordovaCamera.getPicture(overlays.options).then(successOverlay, errorOverlay);
}
//Callback functions
function successOverlay(imageUrl) {
//If user has successfully selected a file
var extension = "jpg";
var filename = getCurrentDateFileName();
$cordovaFileTransfer.download(imageUrl, cordova.file.dataDirectory + filename + '.' + extension, {}, true)
.then(function (fileEntry) {
overlays.dataUrl = fileEntry.nativeURL;
}, function (e) {
errorMessages.push(e);
});
}
function errorOverlay(message) {
//If user couldn't select a file
errorMessages.push(message);
//$rootScope.$apply();
}
}
})();
Now the controller:
(function () {
angular
.module("cameraApp.settings")
.controller("SettingsController", SettingsController);
SettingsController.$inject = ["settingsService"];
function SettingsController(settingsService) {
var vm = this;
vm.settings = settingsService;
activate();
//////////////////
function activate(){
// Nothing here yet
}
}
})();
Finnally on the view:
<h1>{{vm.settings.overlays.dataUrl}}</h1>
<button id="overlay" class="button"
ng-click="vm.settings.selectOverlayFile()">
Browse...
</button>
Whenever I change the value in the factory, it doesn't change in the view.
Thanks in advance!
Unfortunately Factories in angularjs are not meant to be used as two way bindings. Factories and Services are only singletons. They are only there to be used when called.
Ex Factory:
app.factory('itemFactory', ['$http', '$rootScope', function($http, $rootScope) {
var service = {};
service.item = null;
service.getItem = function(id) {
$http.get(baseUrl + "getitem/" + id)
.then(function successCallback(resp) {
service.item = resp.data.Data;
$rootScope.$broadcast("itemready");
}, function errorCallback(resp) {
console.log(resp)
});
};
return service;
}]);
I use the $broadcast so if I call getItem my controller knows to go get the fresh data.
Ex Directive:
angular.module("itemApp").directive("item", ['itemFactory', '$routeParams', '$location', '$rootScope', '$timeout', function (itemFactory, $routeParams, $location, $rootScope, $timeout) {
return {
restrict: 'E',
templateUrl: "components/item.html",
link: function (scope, elem, attr) {
scope.item = itemFactory.item;
scope.changeMade = function(){
itemFactory.getItem(1);
}
scope.$on("itemready", function () {
scope.item = itemFactory.item;
})
}
}
}]);
So as you can see in my code above anytime I need a fresh item I use $broadcast and $on to update my service and directive. I hope this makes sense, feel free to ask any questions.
As pointed by Ohjay44, the factory is not updated on the view. The way to do it is using a directive (also as Ohjay44 said). To use $broadcast, $emit and $on and keep the encapsulation I did what is recommended by John Papa's Angular Style Guide: created a factory (in my case a named it comms).
Here goes the newly created directive (overlay.directive.js):
(function () {
angular
.module('cameraApp.settings')
.directive('ptrptSettingsOverlaysInfo', settingsOverlaysInfo);
settingsOverlaysInfo.$inject = ["settingsService", "comms"];
function settingsOverlaysInfo(settingsService, comms) {
var directive = {
restrict: "EA",
templateUrl: "js/app/settings/overlays.directive.html",
link: linkFunc,
controller: "SettingsController",
controllerAs: "vm",
bindToController: true // because the scope is isolated
};
return directive;
function linkFunc(scope, element, attrs, vm) {
vm.overlays = settingsService.overlays;
comms.on("overlaysUpdate", function (event, overlays) {
vm.overlays = overlays;
});
}
}
})();
I created overlay.directive.html with:
<div class="item item-thumbnail-left">
<img ng-src="{{vm.overlays.dataUrl}}">
<h2>{{vm.overlays.dataUrl}}</h2>
</div>
And finally I put an $emit on the settingsService where the overlay is updated:
(function () {
"use strict";
angular
.module("cameraApp.core")
.factory("settingsService", settingsService);
settingsService.$inject = ["comms", "$cordovaFileTransfer", "$cordovaCamera"];
function settingsService(comms, $cordovaFileTransfer, $cordovaCamera) {
var overlays = {
dataUrl: "",
options: {
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
destinationType: Camera.DestinationType.FILE_URI
}
};
var errorMessages = [];
var service = {
overlays: overlays,
selectOverlayFile: selectOverlayFile,
errorMessages: errorMessages
};
return service;
function selectOverlayFile() {
$cordovaCamera.getPicture(overlays.options).then(successOverlay, errorOverlay);
}
//Callback functions
function successOverlay(imageUrl) {
//If user has successfully selected a file
var extension = "jpg";
var filename = getCurrentDateFileName();
$cordovaFileTransfer.download(imageUrl, cordova.file.dataDirectory + filename + '.' + extension, {}, true)
.then(function (fileEntry) {
overlays.dataUrl = fileEntry.nativeURL;
// New code!!!!
comms.emit("overlaysUpdated", overlays);
}, function (e) {
errorMessages.push(e);
});
}
function errorOverlay(message) {
//If user couldn't select a file
errorMessages.push(message);
//$rootScope.$apply();
}
}
})();
I used an $emit instead of a broadcast to prevent the bubbling as explained here: What's the correct way to communicate between controllers in AngularJS?
Hope this helps someone else too.
Cheers!

Why my value is not updated in the directive's view

I have a value named $scope.title in my controller. This value is initialized with $scope.title = 'global.loading';. I have a factory named Product.
My view is calling a directive via <menu-top ng-title="title"></menu-top>, the view of this directive is <span>{{title|translate}}</span>.
When I want to get a product I do : Product.get(id). Their is two possibility.
First one (working) -> My product is cached in localstorage and my title in the directive is uptated.
Second one (not working) -> My product is not cached, I call my WebService, put the response in cache and return the response. In this case, the title is updated (console.log) in the controller, but not in my directive ...
angular.module('angularApp')
.directive('menuTop', function () {
return {
templateUrl: 'views/directives/menutop.html',
restrict: 'E',
scope:{
ngTitle: '=?'
},
link: function postLink(scope) {
scope.title = scope.ngTitle;
}
};
});
angular.module('angularApp')
.controller('ProductCtrl', function ($scope, $routeParams, Product) {
$scope.productId = parseInt($routeParams.product);
$scope.title = 'global.loading';
$scope.loading = true;
$scope.error = false;
$scope.product = null;
Product
.get($scope.productId)
.then(function(product){
$scope.loading = false;
$scope.title = product.name;
$scope.product = product;
}, function(){
$scope.error = true;
$scope.loading = false;
})
;
});
angular.module('angularApp')
.factory('Product', function ($http, responseHandler, ApiLink, LocalStorage, $q) {
var _get = function(id) {
return $q(function(resolve, reject) {
var key = 'catalog/product/' + id;
var ret = LocalStorage.getObject(key);
if (ret) {
return resolve(ret);
}
responseHandler
.handle($http({
method: 'GET',
url: ApiLink.get('catalog', 'product', {id: id})
}))
.then(function(response) {
if (response.product && response.product.name) {
LocalStorage.putObject(key, response.product, 60 * 5);
return resolve(response.product);
}
reject(null);
}, function() {
reject(null);
});
});
};
return {
'get': _get
};
});
Thank you for your help !
As Sergio Tulentsev suggested, you can use '#' as binding method.
Using # will interpolate the value. It means that you can use it as a readonly this way : ng-title="{{mytitle}}"
angular.module('angularApp')
.directive('menuTop', function () {
return {
templateUrl: 'views/directives/menutop.html',
restrict: 'E',
scope:{
ngTitle: '#'
},
link: function postLink(scope) {
scope.title = scope.ngTitle;
}
};
});
Also keep in mind that you shouldn't use "ng" for your custom directives. ng is used for angular natives components. You can (should) keep this naming convention with your application name. Like for an application "MyStats" your could name your components ms-directivename
If you need more informations about the directives bindings you can refer to this documentation

Avoid repetition in Angular controller

I'm an angular newbie and I'm writing an Ionic app.
I finished my app and am trying to refactor my controller avoiding code repetition.
I have this piece of code that manages my modal:
angular.module('starter')
.controller('NewsCtrl', function($scope, content, $cordovaSocialSharing, $timeout, $sce, $ionicModal){
$scope.news = content;
content.getList('comments').then(function (comments) {
$scope.comments = comments;
});
$scope.addComment = function() {
};
$scope.shareAnywhere = function() {
$cordovaSocialSharing.share("Guarda questo articolo pubblicato da DDay", "Ti stanno segnalando questo articolo", content.thumbnail, "http://blog.nraboy.com");
};
$ionicModal.fromTemplateUrl('templates/comments.html', {
scope: $scope,
animation: 'slide-in-up'
}).then(function(modal) {
$scope.modal = modal;
});
$scope.showComment = function() {
$scope.modal.show();
};
// Triggered in the login modal to close it
$scope.closeComment = function() {
$scope.modal.hide();
};
$scope.$on('modal.shown', function() {
var footerBar;
var scroller;
var txtInput;
$timeout(function() {
footerBar = document.body.querySelector('#commentView .bar-footer');
scroller = document.body.querySelector('#commentView .scroll-content');
txtInput = angular.element(footerBar.querySelector('textarea'));
}, 0);
$scope.$on('taResize', function(e, ta) {
if (!ta) return;
var taHeight = ta[0].offsetHeight;
if (!footerBar) return;
var newFooterHeight = taHeight + 10;
newFooterHeight = (newFooterHeight > 44) ? newFooterHeight : 44;
footerBar.style.height = newFooterHeight + 'px';
scroller.style.bottom = newFooterHeight + 'px';
});
});
});
I have added this same code in 6 controllers.
Is there a way to avoid the repetition?
Probably what you are looking for is an angular service. This component is a singleton object, that you inject in every controller you need to execute this code.
Angular Services
Regards,
Below is an example of a service I created to retrieve address data from a Json file. Here is the working Plunk. http://plnkr.co/edit/RRPv2p4ryQgDEcFqRHHz?p=preview
angular.module('myApp')
.factory('addressService', addressService);
addressService.$inject = ['$q', '$timeout', '$http'];
function addressService($q, $timeout, $http) {
var addresses = [];
//console.log("Number of table entries is: " + orders.length);
var promise = $http.get('address.data.json');
promise.then(function(response) {
addresses = response.data;
// console.log("Number of table entries is now: " + orders.length);
});
return {
GetAddresses: getAddresses
};
function getAddresses() {
return $q(function(resolve, reject) {
$timeout(function() {
resolve(addresses);
}, 2000);
});
}
}
Here's an example of how I added dependencies for it and another service to my controller (This is NOT the only way to do dependency injection, but is my favorite way as it is easier to read). I then called my addressService.GetAddresses() from within my controller.
var app = angular.module('myApp', ['smart-table']);
app.controller('TableController', TableController);
TableController.$inject = [ "orderService", "addressService"];
function TableController( orderService, addressService) {
addressService.GetAddresses()
.then(function(results) {
me.addresses = results;
// console.log(me.addresses.length + " addresses");
},
function(error) {})
.finally(function() {
me.loadingAddresses = false;
});
});}
I also had to include my .js tag in a script element on my index.html.
<script src="addressdata.service.js"></script>

Angular.js share service data between directives

I'm working on my first Angular.js application and I'm a bit confused.
Currently I have two directives that both need the same data to build up the page.
This data is loaded from an external api.
Now currently I have created this factory, which looks like:
(function() {
var app = angular.module('dataService', []);
app.factory('dataService', ['$http', function($http) {
var links = [];
return {
getMenu: function() {
if(links.length > 0) {
return links;
} else {
$http.get('http://localhost/server/api.php?ajax=true&action=getCats').success(function(data) {
return data;
})
}
}
}
}])
})();
But I'm rather confused how to use this service, obviously if there is a $http request, the return will never be called with the correct data.
In my directive I would use it like this:
(function() {
// Menu directive
var app = angular.module('menu', ['dataService']);
app.directive('menu', ['dataService', function(dataService) {
return {
restrict: 'E',
templateUrl: 'scripts/menu/menu.html',
controller: function() {
console.log(dataService.getMenu()); // Return 'undefined'
},
controllerAs: 'menuCtrl'
}
}])
})();
Change your service method so that it handles both synchronous and asynchronous scenarios:
getMenu: function() {
var deferred = $q.defer();
if(links.length > 0) {
deferred.resolve(links);
} else {
$http.get('http://localhost/server/api.php?ajax=true&action=getCats').success(function(data) {
deferred.resolve(data);
})
}
return deferred.promise;
}
Usage:
dataService.getMenu().then(function(data){
console.log(data);
});

Resources