AngularJS Factory object not being updated on controller and view - angularjs

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!

Related

display or hide loading from multiple controllers

I'm trying to create a generic loading that would be shared among controllers. So, I've created a service that will control the hide/show state:
(function () {
'use strict';
var module = angular.module('main');
var serviceId = "LoadingService";
var ShowLoading = false;
function LoadingService($rootScope) {
function showLoading(value) {
$rootScope.$broadcast(LOADING_CHANGED, value);
}
var service = {
ShowLoading: showLoading
}
return service;
}
LoadingService.$inject = ['$rootScope'];
module.service(serviceId, LoadingService);
}());
here's the loading controller:
(function () {
'use strict';
var app = angular.module("main");
var controllerId = "rbLoading.controller";
function loadingController($scope, LoadingService) {
$scope.isLoading = false;
$scope.$on(LOADING_CHANGED, function (event, data) {
$scope.isLoading = data;
});
}
app.controller(controllerId, loadingController);
loadingController.$inject = ['$scope', 'LoadingService'];
}());
the html:
<div class="loading-panel" ng-if="isLoading">
<!-- loading content-->
</div>
and the directive:
(function () {
'use strict';
var loadingDirective = function (OPTIMISATION) {
return {
restrict: 'E',
templateUrl: function (element, attr) {
return 'App/components/shared/loading/loading.html';
},
controller: 'rbLoading.controller',
controllerAs: 'loading'
};
}
var app = angular.module('main');
loadingDirective.$inject = ["OPTIMISATION"];
app.directive('rbLoading', loadingDirective);
}());
When I'm changing the LoadingService value, it's triggering the LOADING_CHANGED event. The problem is that ng-if is not working as expected, although the data variable is with the right value. What am I missing?
UPDATE
Changing to ng-show almost solve the problem. For a particular scenario is not working. I'm displaying the loading while trying to get the user position using html5 GeoLocation api, and setting a timeout after 10 seconds using.
setTimeout(function () {
console.log("timeout");
LoadingService.ShowLoading(false);
}, 10000);
this setTimeout function is being triggered, however the loading keeps being displayed.

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

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>

Changes event does not trigger in angular directive

I'm writing an angular directive. Here is the code:
'use strict';
app.directive('mcategory', function (MainCategory, $rootScope) {
return {
restrict: 'E',
replace: true,
template: '<select data-ng-model="selectedMainCategory" data-ng-options="mainCategory.mainCategoryId as mainCategory.mainCategoryName ' +
'for mainCategory in mainCategories" data-ng-change="mainCategoryChanged()"></select>',
link: function(scope, element, attr, controller) {
var elm = angular.element(element);
elm.attr('id', attr['id']);
elm.attr('name', attr['name']);
elm.attr('class', attr['class']);
MainCategory.get().then(function (mainCategories) {
scope.mainCategories = mainCategories;
});
scope.selectedMainCategory = false;
scope.mainCategoryChanged = function () {
if (!scope.selectedMainCategory) {
return;
}
$rootScope.$broadcast("mainCategoryChanged", { mainCategory: scope.selectedMainCategory });
};
}
};
})
MainCategory is a service and it works ok.
The problem is that selectedMainCategory is always undefined and mainCategoryChanged() event is not triggered when user selects another category in the select element. I think I have made a silly mistake but it is more than 2 hours that I can't solve the problem. Any help is appreciated in advance.
UPDATE
Here is the service:
'use strict';
app.factory('MainCategory', function ($http, $q) {
return {
get: function() {
var deferred = $q.defer();
$http.get('/MainCategory/GetMainCategories').success(deferred.resolve).error(deferred.reject );
return deferred.promise;
}
};
})
When I '/MainCategory/GetMainCategories' url in the browser I get all the categories in the correct json format.

Resources