I have the next 'problem' with Angular 1.
I have this Factory that I use to get the data for the current logged user:
angular.module('myModule')
.factory('authFactory', function ($http, $rootScope, Session, api, backend_url) {
var authFactory = this;
var user = {};
authFactory.init = function(){
// This API returns the information of the current user
api.current_user.get({}).$promise.then(function(res){
user = res;
});
}
// I use this function to return the user
authFactory.user = function () {
return user;
};
}
This is a basic Controller example where I'm trying to access the information retrieved by the above factory:
angular.module('myModule.mypage')
.controller('PageCtrl', function ($scope, authFactory) {
$scope.user = authFactory.user();
authFactory.init();
angular.element(document).ready(function () {
// This will return {} because it's called
// before the factory updates user value
console.log(authFactory.user());
console.log($scope.user);
});
});
The problem is that $scope.user = myFactory.user(); is not being updated once the Factory retrieve the user value.
I think my issue is related with myFactory.user();. I'm using a function, so the value returned by the function is not updated after myFactory.user has changed, I think that's why on PageCtrl the variable $scope.user is not getting any value.
My questions are:
Which is the best approach on my controller to wait until the user info is loaded by authFactory ?
Should I use a service instead ?
Problem with your implementation is that user is being initialized when authFactory.init() is invoked using presumably asynchronous API.
I would suggest you to return promise from authFactory.user method.
angular.module('myModule')
.factory('authFactory', function ($http, $rootScope, Session, api, $q, backend_url) {
var authFactory = this;
var user = {};
authFactory.init = function () {
// This API returns the information of the current user
return api.current_user.get({}).$promise.then(function (res) {
user = res;
});
}
//Return promise from the method
authFactory.user = function () {
var deferred = $q.defer();
if (angular.isDefined(user)) {
deferred.resolve(user);
} else {
authFactory.init().then(function () {
deferred.resolve(user);
});
}
return deferred.promise;
};
});
Then modify controller
angular.module('myModule.mypage')
.controller('PageCtrl', function ($scope, authFactory) {
authFactory.user().then(function (user) {
$scope.user = user;
})
});
angular.module('myModule')
.factory('authFactory', function ($http, $rootScope, Session, api, backend_url) {
var authFactory = this;
authFactory.user = {}
// I use this function to return the user
authFactory.getUser() = function () {
return api.current_user.get({}).$promise.then(function(res){
authFactory.user = res;
});
};
}
angular.module('myModule.mypage')
.controller('PageCtrl', function ($scope, authFactory) {
authFactory.getUser().then(function() {
$scope.user = authFactory.user;
});
});
Provide us a JSFiddle, I tried to help you without any testing environment.
Related
I've written a couple of these tests previously on other controllers and it works just fine. But on this more complex controller, no matter which function I test it on, the .callFake using $q is not entering into the .then block and so no logic in there is being executed at all. I know the controller works for sure as this is on a production website. So why doesn't my test even enter the .then block when I've returned the deferred promise?
Controller Code -
(function() {
'use strict';
angular
.module('myApp')
.controller('EntryAddController', EntryAddController);
function EntryAddController($scope, $state, moment, $sanitize, EntryFactory, $modal, toastr, $log) {
var vm = this;
var now = moment();
var $currentYear = now.year();
vm.queues = [];
vm.calculating = false;
vm.total = 0;
vm.saving = false;
vm.addTxt = 'Confirm Entry';
var tomorrow = now.add(1, 'days').toDate();
var to = now.subtract(1, 'days').add(12, 'months').toDate();
vm.fromDate = tomorrow;
vm.toDate = to;
activate();
////////////////
function activate() {
var queueCache = {};
vm.updateEntrys = function() {
var payload = {
'from': moment(vm.fromDate).format('MM/DD/YYYY'),
'to': moment(vm.toDate).format('MM/DD/YYYY'),
'freq': vm.frequency.value,
'total': vm.total_amount
};
var key = JSON.stringify(payload);
if (!(key in queueCache)) {
EntryFactory.getEntryQueue(payload)
.then(function(resp) {
//LOGIC HERE BUT TEST NEVER ENTERS HERE DESPITE USING $Q
});
} else {
vm.queues = queueCache[key].queue;
vm.total = queueCache[key].total;
vm.calculating = false;
}
}
}
}
})();
Test Code
(function() {
'use strict';
describe('Entry Add Controller Spec', function(){
var vm;
var $controller;
var $q;
var $rootScope;
var EntryFactory;
var $scope;
var toastr;
beforeEach(module('myApp'));
beforeEach(inject(function(_$controller_, _$q_, _$rootScope_, _EntryFactory_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$scope = _$rootScope_.$new();
$q = _$q_;
EntryFactory = _EntryFactory_;
vm = $controller('EntryAddController', { $scope: $scope });
}));
it('expect EntryFactory.getEntryQueue to correctly set queues and total upon successful response', function() {
var payload = "blah";
var resp = {
"data": 1
}
spyOn(EntryFactory, 'getEntryQueue').and.callFake(function(payload) {
var deferred = $q.defer();
deferred.resolve(resp);
return deferred.promise;
});
EntryFactory.getEntryQueue(payload);
$rootScope.$apply();
//expect logic in .then to have worked
});
});
})();
Edit
Just thought of something... is this because I'm calling the factory function (EntryFactory.getEntryQueue) directly in the test, instead of calling the vm.updateEntrys function around it and therefore it doesn't ever proceed onto the .then portion of the code?
You allways need to consider what your SUT (System/Software under test) is.
In this case it is a controller's component method vm.updateEntrys.
It calls your Mocked EntryFactory.getEntryQueue() method. Also your code is not complete (where do you define vm.frequency object?). Nevertheless I have tested this method with the following test :
/*property
$apply, $log, $modal, $new, $sanitize, $scope, $state, EntryFactory, and,
callFake, data, defer, getEntryQueue, moment, promise, resolve, toastr,
updateEntrys
*/
(function () {
'use strict';
describe('Entry Add Controller Spec', function () {
var vm;
var $controller;
var $q;
var $rootScope;
var EntryFactory;
var $scope;
var toastr;
beforeEach(module('app.controllers'));
beforeEach(inject(function (_$controller_, _$q_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$q = _$q_;
}));
beforeEach(function () {
$scope = $rootScope.$new();
EntryFactory = {
getEntryQueue: function () {
}
};
moment = function () {
return {
format: function () {
}
}
}
vm = $controller('EntryAddController', { '$scope': $scope, '$state': {}, 'moment': moment, '$sanitize': {}, 'EntryFactory': EntryFactory, '$modal': {}, 'toastr': {}, '$log': {} });
});
it('expect EntryFactory.getEntryQueue to correctly set queues and total upon successful response', function () {
var payload = "blah";
var resp = {
"data": 1
};
spyOn(EntryFactory, 'getEntryQueue').and.callFake(function (payload) {
var deferred = $q.defer();
deferred.resolve(resp);
return deferred.promise;
});
vm.updateEntrys();
EntryFactory.getEntryQueue(payload);
$scope.$apply();
//expect logic in .then to have worked
});
});
})();
I have a separate model and a controller for a teachers list.
My teacherModel.js is:
app.factory('Teacher', [function() {
function Teacher(teacher) {
// constructor
};
Teacher.prototype = {
setTeacher: function(teacher) {
angular.extend(this, teacher);
},
getAllTeachers: function(callback) {
var scope = this;
var ref = firebase.database().ref('/xxx/teachers');
ref.once('value').then(function(snapshot) {
teachersList = snapshot.val();
scope.setTeacher(teachersList);
// THERE'S A PROBLEM HERE...
// I'm trying to pass this callback from the controller:
callback;
});
}
};
return Teacher;
}]);
Now from my controller I call the getAllTeachers() method with a callback function:
app.controller('teacherMainCtrl', ['$scope', 'Teacher', function($scope, Teacher){
var teacher = new Teacher()
teacher.getAllTeachers(function() {
$scope.teachers = teacher;
console.log($scope.teachers);
});
}]);
Problem is console.log($scope.teachers); is not logging anything to the console. I don't think the callback is being executed at all.
Can someone help me to figure out what I'm doing wrong or suggest a better way to add functionality to the model data from controller after the data is asynchronously retrieved from firebase? Thanks.
You can leverage the fact that once returns a firebase promise so you can alter your code to the following:
app.factory('Teacher', [function() {
function Teacher(teacher) {
// constructor
};
Teacher.prototype = {
setTeacher: function(teacher) {
angular.extend(this, teacher);
},
getAllTeachers: function() {
var scope = this;
var ref = firebase.database().ref('/xxx/teachers');
return ref.once('value').then(function(snapshot) {
return snapshot.val();
});
}
};
return Teacher;
}]);
This would behave similarly to any $http request where it returns a promise. Now, in your controller, you can then call your getAllTeachers() like so:
app.controller('teacherMainCtrl', ['$scope', 'Teacher', function($scope, Teacher){
var teacher = new Teacher()
teacher.getAllTeachers().then(function (snapshotValues) {
// What you returned in the promise above is populated in snapshotValues here
$scope.teachers = snapshotValues;
});
}]);
Update
If you want to use the $q service for your particular scenario, you can do the following:
app.factory('Teacher', ['$q', function($q) {
function Teacher(teacher) {
// constructor
};
Teacher.prototype = {
setTeacher: function(teacher) {
angular.extend(this, teacher);
},
getAllTeachers: function() {
var defer = $q.defer();
var scope = this;
var ref = firebase.database().ref('/xxx/teachers');
ref.once('value').then(function(snapshot) {
var val = snapshot.val();
// Transform your data any way you want.
// Whatever you pass into resolve() will be available as a parameter in the subsequent then()
defer.resolve(val);
});
return defer.promise;
}
};
return Teacher;
}]);
Using the method would still be the same. You simply just call then()
teacher.getAllTeachers()
.then(function (whatYouPassedInResolve) {
});
Another thing to note is that in the getAllTeachers method inside of your factory, I did not handle any error cases. That would be achieved by rejecting the promise with defer.reject(objectToSendBack). You pass in any data you want accessible when you deem that call a failure.
Just pass in a function for the second parameter to the `then(successCallback, errorCallback) to handle any rejected promises.
I think you are not calling the callback actually, use callback()
app.factory('Teacher', [function() {
function Teacher(teacher) {
// constructor
};
Teacher.prototype = {
setTeacher: function(teacher) {
angular.extend(this, teacher);
},
getAllTeachers: function(callback) {
var scope = this;
var ref = firebase.database().ref('/xxx/teachers');
ref.once('value').then(function(snapshot) {
teachersList = snapshot.val();
scope.setTeacher(teachersList);
// THERE'S A PROBLEM HERE...
// Try this
callback();
});
}
};
return Teacher;
}]);
I want to call a function or change the value of variable/s which is there inside another controller. I looked online for the solution and understood that I have to create a service and use it inside both the controller, however I am not able to understand that how service will have access to $scope.home_main and $scope.home_main variables as they are in different scope.
JS
app.controller('Controller1', function ($scope, $window) {
$scope.toggle = function() {
$scope.home_main = !$scope.home_main;
$scope.full_page_place_holder = !$scope.full_page_place_holder;
};
});
app.controller('Controller2', function ($scope, $window) {
$scope.onTabSelect=function(){
// here I want to call toggle function which is inside another controller.
};
});
Updated HTML
<div ng-controller="Controller1">
<div ng-hide="home_main"></div>
</div>
<div ng-controller="Controller2">
<div ng-hide="full_page_place_holder"></div>
</div>
Looked here: SO-Q1, SO-Q2. Thanks.
you can create a service as follows,
angular.module('someApp').service('shareDataService', function() {
var popup;
var setDetails = function(param) {
popup = param;
};
var getDetails = function() {
return popup;
};
return {
setDetails: setDetails,
getDetails: getDetails,
};
});
This service will not have access to the $scope variables of the two controllers, instead you can call getDetails and setDetails to get and set the variable in the service.
suppose you want to send the value of 'home_main' from controller1 to controller2, in controller1, you call the service function setDetails
app.controller('Controller1', function ($scope, $window, shareDataService) {
$scope.toggle = function() {
$scope.home_main = !$scope.home_main;
$scope.full_page_place_holder = !$scope.full_page_place_holder;
shareDataService.setDetails($scope.home_main);
};
});
and in controller2, you get the value by calling the service
app.controller('Controller2', function ($scope, $window) {
var home_main_value = shareDataService.getDetails();
});
You can do a similar thing for functions also.....
Hope it helps
You misunderstood the concept service will have a single variable that is going to be shared by two controllers:-
$scope is local for controller and cannot accessed by another controller:-
FOR Example:-
myApp.factory('Data', function () {
var data = {
home_main : ''
};
return {
gethome_main : function () {
return data.home_main ;
},
sethome_main : function (home_main ) {
data.home_main = home_main ;
}
};
myApp.controller('FirstCtrl', function ($scope, Data) {
$scope.home_main= '';
$scope.$watch('home_main', function (newValue, oldValue) {
if (newValue !== oldValue) Data.sethome_main(newValue);
});
});
myApp.controller('SecondCtrl', function ($scope, Data) {
$scope.$watch(function () { return Data.gethome_main(); }, function (newValue, oldValue) {
if (newValue !== oldValue) $scope.home_main= newValue;
});
});
I have the following service:
app.service('Library', ['$http', function($http) {
this.fonts = [];
this.families = [];
// ... some common CRUD functions here ...
// Returns the font list
this.getFonts = function() {
if(_.isEmpty(this.fonts)) this.updateFonts();
return this.fonts;
};
// Returns the family list
this.getFamilies = function() {
if(_.isEmpty(this.families)) this.updateFamilies();
return this.families;
};
// Update the font list
this.updateFonts = function() {
var self = this;
$http.get(BACKEND_URL+'/fonts').success(function(data) {
self.fonts = data;
console.log('Library:: fonts updated', self.fonts);
});
};
// Update the family
this.updateFamilies = function() {
var self = this;
$http.get(BACKEND_URL+'/families').success(function(data) {
var sorted = _.sortBy(data, function(item) { return item });
self.families = sorted;
console.log('Library:: families updated', self.families);
});
};
}]);
And the following main controller code:
app.controller('MainController', ['$scope', '$state', 'Cart', 'Library', function($scope, $state, Cart, Library) {
console.log('-> MainController');
// Serve the right font list depending on the page
$scope.fonts = $state.is('home.cart') ? Cart.getFonts() : Library.getFonts();
$scope.families = Library.getFamilies();
}]);
The problem is, that when the view requests the content of $scope.fonts, it's still empty.
How to update $scope.fonts and $scope.families when the loading is over?
I could use $scope.$watch but I'm sure there is a cleaner way to do it...
This really is what promises were made for. Your service should return a promise that is to be resolved. You could also simplify your service:
app.service('Library', ['$http', '$q', function($http, $q) {
var self = this;
self.families = [];
// Returns the family list
self.getFamilies = function() {
var deferred = $q.defer();
if(_.isEmpty(self.families)) {
$http.get(BACKEND_URL+'/families').success(function(data) {
var sorted = _.sortBy(data, function(item) { return item });
self.families = sorted;
deferred.resolve(self.families);
console.log('Library:: families updated', self.families);
});
} else {
deferred.resolve(self.families);
}
return deferred.promise;
};
}]);
And then in your controller, use the promises then method:
app.controller('MainController', ['$scope', '$state', 'Cart', 'Library', function($scope, $state, Cart, Library) {
console.log('-> MainController');
// Serve the right font list depending on the page
$scope.fonts = $state.is('home.cart') ? Cart.getFonts() : Library.getFonts();
Library.getFamilies().then(function(result) {
$scope.families = result;
});
}]);
This is untested because of the $http, but here is a demo using $timeout:
JSFiddle
Consider passing a callback function.
Service:
this.getFonts = function(callback) {
if(_.isEmpty(this.fonts)) this.updateFonts(callback);
return this.fonts;
};
this.updateFonts = function(callback) {
var self = this;
$http.get(BACKEND_URL+'/fonts').success(function(data) {
self.fonts = data;
console.log('Library:: fonts updated', self.fonts);
callback(data);
});
};
Controller:
Library.getFonts(function (data) { $scope.fonts = data; });
This could be tidied up a bit, since a callback eliminates the need for some of this code, but it'll serve as an example.
Thanks for all the answers! I ended up using a mix of callback and promise, as follow:
app.service('Library', function($http) {
// Returns the font list
this.getFonts = function(callback) {
if(_.isEmpty(self.fonts)) return self.updateFonts(callback);
else return callback(self.fonts);
};
// Update the font list
this.updateFonts = function(callback) {
return $http.get(BACKEND_URL+'/fonts').success(function(data) {
self.fonts = data;
callback(data);
});
};
});
And, in the controller:
app.controller('MainController', function(Library) {
Library.getFonts(function(fonts) { $scope.fonts = fonts });
});
I tried all your suggestions, but this is the best one working with the rest of my code.
In your this.getFonts function (and your other functions), you call the data from this, which points to the function instead of the controller scope you want. Try the following instead:
var self = this;
self.fonts = [];
self.families = [];
// ... some common CRUD functions here ...
// Returns the font list
self.getFonts = function() {
if(_.isEmpty(self.fonts)) self.updateFonts();
return self.fonts; // <-- self.fonts will point to the fonts you want
};
I would try wrapping your getScope and getFonts bodies that you are calling in a
$scope.$apply(function(){ ...body here... });
Make sure you declare self = this outside any functions.
Assign the call to the value you want to store the data in and then return it.
var self = this;
self.data = [];
this.updateFonts = function() {
self.fonts = $http.get(BACKEND_URL+'/fonts').success(function(data) {
return data.data
});
return self.fonts
};
Since you're using ui-router (i saw a $state). You can use a resolve in your state and return a promise.
Doc : https://github.com/angular-ui/ui-router/wiki
Exemple :
$stateProvider.state('myState', {
resolve:{
// Example using function with returned promise.
// This is the typical use case of resolve.
// You need to inject any services that you are
// using, e.g. $http in this example
promiseObj: function($http){
// $http returns a promise for the url data
return $http({method: 'GET', url: '/someUrl'});
}
},
controller: function($scope,promiseObj){
// You can be sure that promiseObj is ready to use!
$scope.items = promiseObj.data;
}
}
In your case you'll need to turn your this.getFonts and getFamilies into promises
this.getFonts = function(){
return $http.get(BACKEND_URL+'/fonts').success(function(data) {
self.fonts = data;
console.log('Library:: fonts updated', self.fonts);
});
}
There is many many way to do this, but in my opinion the resolve way is the best.
Maybe this question has already been asked, but I searched and tried most of my afternoon without any success so I really hope somebody can help me with this.
I want to be able to update my $http.get() - my data - that I have set in a factory service, every few seconds.
I added some comment to my code and also left some old stuff for you guys to see what I have tried. (the old stuff is also commented out)
My code:
ovwid.factory('recentClients', [
'$http',
'$rootScope',
function ($http, $rootScope) {
var apiURL = '/plugins/data/get_client.php';
var promise;
var recentClients =
{
async: function()
{
if ( !promise )
{
// $http returns a promise, which has a 'then' function, which also returns a promise
promise =
$http.get(apiURL)
.then(function (response) {
// The then function here is an opportunity to modify the response
// The return value gets picked up by the then in the controller.
return response.data;
});
}
// Return a promise to the controller
return promise;
}
}
return recentClients;
}]);
ovwid.controller(‘client’Ctrl, [
'$scope',
'recentClients',
'$interval',
function ($scope, recentClients, $interval) {
$scope.loading = true;
function reloadData() {
// a call to the async method
recentClients().async().then(function(data) {
// console.log(data);
$scope.loading = false;
$scope.client = data;
});
}
// Initizialize function
reloadData();
// Start Interval
var timerData =
$interval(function () {
reloadData();
}, 1000);
// function myIntervalFunction() {
// var cancelRefresh = $timeout(function myFunction() {
// reloadData();
// console.log('data refres')
// cancelRefresh = $timeout(myFunction, 5000);
// },5000);
// };
// myIntervalFunction();
// $scope.$on('$destroy', function(e) {
// $timeout.cancel(cancelRefresh);
// });
}]); // [/controller]
I see several issues.
First:
if ( !promise ) is only going to return true the first time. You are assigning it to the $http call.
Secondly:
You never access the async method in your factory.
You either need to return that from factory return recentClients.async or call it from scope recentClients.async().then(..
may be it will help
function reloadData() {
// a call to the async method
$scope.loading = true;
recentClients().then(function(data) {
// console.log(data);
$scope.loading = false;
$scope.client = data;
});
}
// Start Interval
var timerData =
$interval(function () {
if(!$scope.loading){
reloadData();
}
}, 1000);
A few things :)
recentClients().then(function(data)... will not work, in your current code it should be: recentClients.async().then(function(data)
(same remark would apply to ` and ’ qoutes that can get really tricky.
This is the syntax I use for designing services:
ovwid.factory('recentClients', ['$http', '$rootScope', function ($http, $rootScope) {
var apiURL = 'aaa.api';
var recentClients = function() {
return $http.get(apiURL)
}
return {
recentClients : recentClients
};
}]);
Full example:
(just create aaa.api file with some dummy data, fire up a server and you'll see that data is changing)
<!DOCTYPE html>
<html>
<head>
<title>Sorting stuff</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular.min.js"></script>
<script>
var ovwid = angular.module("ovwid", []);
ovwid.factory('recentClients', ['$http', '$rootScope', function ($http, $rootScope) {
var apiURL = 'aaa.api';
var recentClients = function() {
return $http.get(apiURL)
}
return {
recentClients : recentClients
};
}]);
ovwid.controller('clientCtrl', [
'$scope',
'recentClients',
'$interval',
function ($scope, recentClients, $interval) {
$scope.loading = true;
function reloadData() {
// a call to the async method
recentClients.recentClients().then(function(response) {
// console.log(data);
$scope.loading = false;
$scope.client = response.data;
});
}
// Initizialize function
reloadData();
// Start Interval
var timerData =
$interval(function () {
reloadData();
}, 1000);
}]);
</script>
</head>
<body ng-app="ovwid" ng-controller="clientCtrl">
{{ client }}
</body>
</html>
You can set up a service to perform periodic server calls for you. I had found this code somewhere awhile back and refined it a bit. I wish I could remember where I got it.
angular.module('my.services').factory('timeSrv',['$timeout',function($timeout){
//-- Variables --//
var _intervals = {}, _intervalUID = 1;
//-- Methods --//
return {
setInterval : function(op,interval,$scope){
var _intervalID = _intervalUID++;
_intervals[_intervalID] = $timeout(function intervalOperation(){
op($scope || undefined);
_intervals[_intervalID] = $timeout(intervalOperation,interval);
},interval);
return _intervalID;
}, // end setInterval
clearInterval : function(id){
return $timeout.cancel(_intervals[id]);
} // end clearInterval
}; // end return
}]); // end timeSrv
And then in your controller you'd make a call like so:
$scope.getSomethingID = timeSrv.setInterval(function($scope){
[... Do stuff here - Access another service ...]
},10000,$scope);
This will execute the passed function every 10 seconds with the scope of the controller. You can cancel it at anytime by:
timeSrv.clearInterval($scope.getSomethingID);