AngularJS $state.go executes before previous statement execution completes - angularjs

I am using AngularJS, ui-router and $resource for RESTful webservices.
A button in html view is clicked that calls below function i.e. $scope.login(). Consequently a REST service (through $resource) is called and returns a user in case user/pass are correct,
$scope.login = function() {
myfactory.get({
email: $scope.user.email,
password: $scope.user.password
},function(user) {
accessmgr.grantAccess(user); //Line of interest - loi1
$state.go('app.dashboard-v1'); //Line of interest2 - loi2
}, function(x) {
if (x.status == 401)
$scope.authError = 'Email or Password not right';
else
$scope.authError = 'Server Error! Are you connected to internet?';
});
}
in case above successfully executes, another factory function (loi1 above) is called to store user instance in $localStorage as below;
myapp.factory('accessmgr', function($localStorage) {
//var User = {};
return {grantAccess: function(usr) {
$localStorage.user = usr;
}
}});
and ui-router $scope.go(...) takes the user to dashboard.
Problem:
Sometimes $state.go(...) executes before accessmgr.grantAccess(...) causing exceptions as the new state reads user from $localStorage that is not yet written. Reload the page manually solves the problem.
Any help would be really appreciated.

localStorage itself works in synchronous manner, but ngStorage's $localstorage doesn't. The latter is intended to be used in conjunction with scope and is tied to Angular digest cycles. My guess is that
myapp.factory('accessmgr', function($localStorage) {
return {grantAccess: function(usr) {
$localStorage.user = usr;
$localStorage.$apply();
}
}});
may help. ngStorage doesn't really shine when being used like this, probably JS generic library like store.js applies better.
A good alternative is to use model that acts as single source of truth and dumps the data to localStorage under the hood. Depending on the scale of the project, js-data-angular can be considered a solid solution for that.

ngStorage's $localStorage cannot be referred directly without using watchers (not recommended as per here, alternatively it can to be passed as a reference to hook to $scope as mentioned as recommended approach here.
For me, I was using $localStorage through a factory and I tied it to rootScope as below;
$rootScope.$storage = $localStorage;
and consequently
myapp.factory('accessmgr', function($localStorage) {
$rootScope.$storage = $localStorage;
return {
grantAccess: function(usr) {
$rootScope.$storage.user = usr;
},
getUser: function() {
return $rootScope.$storage.user;
}
}});

Related

How can I dry up Angularfire session authorization ($onAuthStateChanged)

Currently I have an authorization system to track user log in/out status using angularfire. The guide I'm looking at suggests using $onAuthStateChanged in every controller as so.
$rootScope.authObj.$onAuthStateChanged(function(firebaseUser) {
if (firebaseUser) {
console.log("Signed in as:", firebaseUser.uid);
});
} else {
console.log("Signed out");
}
});
Instead I've been using a $rootScope.session variable to keep track of my currently logged in user. This works great for the most part, but I can't access this session variable at the start of other controllers since the object is not instantiated at that point. Is there a clean way to access this session variable in the scope of the controller so that I don't have to make an new reference to database in each and every function (as those functions are called after the session variable is set).
To make things a bit more clear.
ref = firebase.database().ref("users/" + $rootScope.session.id + '/meetings');
list = $firebaseArray(ref);
At the top of my controller does not work as $rootScope.session.id is not set yet.
But
$scope.addMeeting = function() {
ref = firebase.database().ref("users/" + $rootScope.session.id + '/meetings');
list = $firebaseArray(ref);
list.$add({
name: $scope.meetingname,
date: firebase.database.ServerValue.TIMESTAMP
});
};
Does work as the function called on a button click which will always be after the page has already loaded, thus meaning $rootScope.session.id is set by that point.
-------------Update-----------------
I've gotten it to work using the firebases suggested methodology, but it doesn't look pretty. It involves nesting everything within a listener on firebase's Auth object and then using an if statement to ensure user object is not null.
myApp.controller('MeetingsController', ['$scope', '$rootScope', '$firebaseAuth', '$firebaseArray', function($scope, $rootScope, $firebaseAuth, $firebaseArray){
var authObj = $firebaseAuth();
authObj.$onAuthStateChanged(function(firebaseUser) {
if (firebaseUser) {
var ref = firebase.database().ref("users/" + firebaseUser.uid + '/meetings');
var meetings = $firebaseArray(ref);
$scope.addMeeting = function() {
meetings.$add({
name: $scope.meetingname,
date: firebase.database.ServerValue.TIMESTAMP
});
};
$scope.deleteMeeting = function(key) {
meetings.$remove(meetings.$getRecord(key)).then(function(ref) {
})
.catch(function(error){
console.log(error);
});
};
}
}); //onAuthStateChange
}]);
$rootScope is a bad option for this kind of storage since it gets cleaned up every time you refresh your page.
You should be lookign into ngStorage. It comes with $localStorage and $sessionStorage, take a read to see what fits better to your needs.
Then add ngStorage to your module and inject $localStorage to the controllers.
$localstorage.sessionId = id;
This will store the id in your browser.
angularfire will track the sessions for you and maintain the current user information. If you are checking for auth in the resolve of each of your states, you can pass the authenticated user into each of the controllers... there is no need for local storage since the underlying firebase SDK is handling that for you.
it might be helpful to provide additional information on the guide you are using.
this documentation here https://github.com/firebase/angularfire/blob/master/docs/guide/user-auth.md#authenticating-with-routers is old, but the pattern can still be used effectively
Create a factory to store the auth properties. Something like this.
app.factory('authService', function(){
return{
authenticated: false
};
});
and then check for it in the controllers:
$scope.authenticated = authService.authenticated;
Hope this somehow helps
P.S controller only used to display model to view and any functionalities should be moved in separate directives or services. Your controller needs a clean up in the future.

How do I cache the response to my promise?

I'm creating an hybrid app with Ionic that will load some JSON files that are stored on the device. Since the same data will be used in several different states, I thought it would make sense to store the response to the JSON request and reuse it, rather than re-reading the JSON file over and over.
This question seems to address that scenario, but I can't seem to get it to work. Although the template works when I used a simpler $http.get().success() request, it never fills in since I started trying to use this service.
app.factory('localJsonService', function($http, $q) {
var localJsonService = {};
localJsonService.returnLegislators = function() {
if (this.legislators) {
return $q.when(this.legislators);
}
return $http.get('/data/legislators.json').then(function(response) {
this.legislators = response.data;
return this.legislators;
});
}
return localJsonService;
});
//old malfunctioning controller
app.controller('profileController', function($scope, $stateParams, localJsonService) {
$scope.legislators = localJsonService.returnLegislators();
$scope.legislator = $scope.legislators[$stateParams.seq_no-1];
console.log($scope.legislator); //displays undefined
});
//EDIT: newer, working controller (but still loads JSON file on each new state)
app.controller('profileController2', function($scope, $stateParams, localJsonService) {
localJsonService.getLegislators().then(function(legislators){
$scope.legislator = legislators[$stateParams.seq_no-1];
});
});
Is it just a simple change to the service that I'm missing? Or am I going about this the wrong way entirely? I'm running AngularJS v1.3.13, but I'm not opposed to a different version, if that will help.
Thanks for any help you can give me.
Use a promise callback and assign your variables in that callback:
localJsonService.returnLegislators().then(function(legislators){
$scope.legislators = legislators;
$scope.legislator = legislators[$stateParams.seq_no-1];
console.log($scope.legislator);
});
If the service data response is not changing, I'd rather user localStorage to cache your response. I'll suggest you ngStorage, that makes it really easy to use localStorage and sessionStorage.
P.S: if datas are changing, then use sessionStorage, that is persistant upon session, but cleaned after app restart.
Example after injecting $localStorage:
Set a default value :
var jsonDefaultVariable = {};
jsonDefaultVariable["myDatas"] = false;
$localStorage.$default(jsonDefaultVariable);
Check for cache :
if($localStorage["myDatas"] !== false){
factory.myDatas = $localStorage.myDatas;
}else{
$http(....).success(function(data){
$localStorage.myDatas = data;
factory.myDatas = data;
});

View updates everywhere but in current controller - angularJS

EDIT: As asked, I'll explain a bit more efficiently !
I've been sitting in front of an annoying problem recently, which is that whenever I update a value inside a directive, the controllers I'm not currently "in" are the only ones to be updated properly.
Scenario example: Profile page is made of two controllers. Navbar_controller which is just currently displaying the user name :
<div ng-if="Auth.isAuthenticated">Hello, {{Auth.getCurrentUser().name}}</div>
The second controller , Profile_controller is here to update user values. This is a simple function in the angular first controller, which updates CurrentUser:
$scope.updateUser = function (type, form) {
if (!$scope.modif)
return ;
$http.put('/api/users/' + Auth.getCurrentUser()._id + '/update', {type:type, modif:$scope.modif})
.success(function (data, status) {
$scope.user = Auth.setNewUser(data);
})
.error(function () {
console.log("error");
});
};
When I update, for example, the name. I can see that the database has been modified properly. And indeed, navbar_controller got the update because a new name is printed in the div. However, Profile_controller doesn't get the update: the name printed in the profile page didn't change.
Here are the two basic functions in Auth.service.js :
getCurrentUser: function() {
return currentUser;
},
// 'user' is the data retrieved in http put request dot success
setNewUser: function(user) {
currentUser = user;
$rootScope.$broadcast(); // Navbar_controller is updated with or without this line
return currentUser;
}
Anyway, if I look at the navbar and its controller, which is calling Auth.getCurrentUser() method, the user values are instantly modified. I'e been using an ugly method consisting in modifying the controller values manually or by refreshing the page... But this isn't the way to go, right ?
There must be something with "$rootScope.$broadcast();", but I'm really new to Angular and other questions on stackoverflow are too specific to help me understand properly.
Thank you !
Your question was a little difficult to understand, but I think the problem is that you are reference a changing object in your various controllers. Here is an example to explain:
Service:
var myObject = { ... };
return {
getObject() { return myObject; }
setObject(obj) { myObject = obj; }
};
Controller 1:
$scope.myObjA = Service.getObject();
Controller 2:
$scope.myObjB = Service.getObject();
Now on initialisation both controllers will be referencing the same object, so if you changed a property inside either controller (eg. $scope.myObjB.name = 'bob';), then the other controller would also see the name.
However if you changed the object itself in a controller (eg. Service.setObject(newObj);), then the controller will be referencing the new object, while the other controller will still be referencing the old one.
You can fix this by wrapping your service object in a container:
var cont = {
user: ...
};
function getContainer() { return cont; }
function setNewUser(user) { cont.user = user; }
Then inside your controllers, get the container (not the user):
$scope.cont = Service.getContainer();
And inside your html:
<div>{{cont.user.name}}</div>
Now when you update the user, all attached controllers will be updated.
Well I'd try to change and store the user information in $rootScope, for your scenario could be a good fit.
getCurrentUser: function() {
$rootScope.currentUser===undefined ? 'no User': $rootScope.currentUser;
},
setNewUser: function(user) {
$rootScope.currentUser = user;
//$rootScope.$broadcast(); no need to broadcast
return getCurrentUser();
}
in that way currentUser will be updated in different scopes as needed!
I'll quote AnuglarJs FAQ regarding to $rootscope:
$rootScope exists, but it can be used for evil
Occasionally there are pieces of data that you want to make global to
the whole app. For these, you can inject $rootScope and set values on
it like any other scope. Since the scopes inherit from the root scope,
these values will be available to the expressions attached to
directives like ng-show just like values on your local $scope.
Of course, global state sucks and you should use $rootScope sparingly,
like you would (hopefully) use with global variables in any language.
In particular, don't use it for code, only data. If you're tempted to
put a function on $rootScope, it's almost always better to put it in a
service that can be injected where it's needed, and more easily
tested.
Conversely, don't create a service whose only purpose in life is to
store and return bits of data.

Sharing data between controllers and dealing with update/change notifications

I'm trying to get my head around sharing data between multiple controllers, but couldn't find out yet how this is supposed to work (the angular way). I have create a Data service that look something like this:
angular.module('myapp.services')
.service('DataSet', function($rootScope) {
return {
filter: function(filterMethod) {
/// ... do async stuff
$rootScope.$broadcast("Data::filtered");
},
brush: function(brushed) {
/// ... do async stuff
$rootScope.$broadcast("Data::brushed");
},
load: function() {
/// ... do async stuff
$rootScope.$broadcast("Data::loaded");
}
};
});
Next I want to reuse and update data from this service, so I use it in my controller as follows:
angular.module('myapp.controllers')
.controller('FilterCtrl', function ($scope, $rootScope, DataSet) {
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
function updateBrushed() {
$scope.safeApply(function() {
$scope.brushed = DataSet.brushed;
});
};
$scope.brushed = [];
$scope.keepSelected = function() {
DataSet.filter(DataSet.FilterMethod.KEEP);
};
$scope.removeSelected = function() {
DataSet.filter(DataSet.FilterMethod.REMOVE);
};
$scope.$on('Data::brushed', updateBrushed);
$scope.$on('Data::filtered', updateBrushed);
});
The problem I have is basically illustrated by the use of the saveApply call. Basically I got this code from here: https://coderwall.com/p/ngisma. What I don't understand though is why I need it. As far as I can see, I'm 'within' $angular when updating the DataSet service. Nevertheless, the view for the Filter controller doesn't get updated without a call to saveApply ($apply doesn't work at all because than I run into the apply already in progress issue).
So, basically my question boils down to: is the approach above a good way to share data, and if so how is notification of changes in the service supposed to work?
Update: Based on Julian Hollman his suggestion I came to the following solution: http://jsfiddle.net/Ljfadvru/7/. This more or less illustrates the full workflow I was working on, though some of it is automatically induced in the fiddle, as opposed to user-interaction based in my real application. What I like about this approach is that it only sends signals when all data is updated.
Working with references, as suggested by Ed Hinchliffe, is nice as well. However, I'm working on a web visualization framework and I'm expecting tens of thousands of items. Clearing arrays and pushing new elements (which seem to me the consequence of this proposal) is really not feasible (if I understand this paradigm well, it would also result in a re-rendering of my vis for every single change). I stand corrected though if there are suggestions for further improvement.
$broadcast doesn't trigger an $apply and I bet your "async stuff" is not $http from angular.
So something happens outside of angular and angular doesn't know that something has changed.
In my opinion the best thing in that case is to write a wrapper for your async code and trigger $apply after date came back from the backend. Don't do it in the controller.
To be honest, I'm not sure quite sure about exactly what is going on with the digest loops in your particular scenario, but I don't think you are approaching this the right way.
The 'angular' way, is to use promises.
Your service should be more like this:
angular.module('myapp.services')
.service('DataSet', function($rootScope) {
return {
filter: function(filterMethod) {
var returnData = []
$http.get('/some/stuff').then(function(data){
for(i in data){
returnData.push(data[i]);
}
});
return returnData;
}
};
});
This sets up an empty placeholder object (returnData) that can be immediately passed to the controller, but a reference is kept so that when the data returns you can retrospectively populate that object. Because the controller and the service reference the same object, it'll 'just work'.
This way you don't have to worry about dealing with $digest or $apply or $broadcast.
You controller can just call $scope.filtered = DataSet.filter();
EDIT
If you want to be able to access the exact same data from multiple controllers:
angular.module('myapp.services')
.factory('DataSet', function($http) {
var cache = {
filtered: []
}
return {
getFiltered: function(){
if(cache.filtered.length) return cache.filtered;
$http.get('/some/url/').then(function(data){
for(i in data){
cache.filtered.push(data[i]);
}
});
}
};
});

Angularjs : Why I need to click on a button to get my view updated?

I'm trying to developpe a chrome extension with angularjs and I have a strange behaviour when I try to initialize the $scope with the url of the active tab.
Here the code of my controller:
var app = angular.module('app', ['app.service']);
app.controller('ItemCtrl', function ($scope, chromeHelper) {
$scope.website = "No result!";
// Does not work until I click on something :-/
chromeHelper.getActiveTabDomain(function (domain) {$scope.website = domain; });
});
So when I try to initialize directly the $scope.website member it doesn't succeed but when I click on the button aftewards $scope.website then updates.
I really don't understand why.
Here is the code of my Chromehelper service:
var service = angular.module('app.service', []);
service.factory('chromeHelper', function() {
var chromeHelper = {};
chromeHelper.getActiveTabDomain = function (callback){
chrome.tabs.query({'active': true}, function(tabs){
if(tabs && tabs.length > 0) callback(getDomainFrom(tabs[0].url));
});
};
return chromeHelper;
});
function getDomainFrom(url) {
return url.match(/:\/\/(.[^/]+)/)[1];
}
Thank you very much in advance!
The OP solved the problem (see comment above) by adding $scope.$apply() at the end of the callback:
// Does not work until I click on something :-/
chromeHelper.getActiveTabDomain(function(domain) {
$scope.website = domain;
$scope.$apply(); // <-- adding this line did the trick
});
A short explanation for anyone landing on this page with a similar problem:
From the AngularJS docs on 'scope' (more specifically from the section titled 'Scope Life Cycle'):
Model mutation
For mutations to be properly observed, you should make them only within the scope.$apply(). (Angular APIs do this implicitly, so no extra $apply call is needed when doing synchronous work in controllers, or asynchronous work with $http or $timeout services.
See, also, this short demo.

Resources