How to preserve ui-router parameters on browser refresh - angularjs

am using angular ui-router to manage states of my SPA.
I have this route:
.state('index.administration.security.roleEdit', {
url: '/roleEdit',
templateUrl: 'app/administration/security/role/roleEdit.html',
controller: 'roleEditCtrl',
controllerAs: 'roleEditCtrl',
params: { data: null },
resolve: {
role: function ($stateParams) {
return angular.fromJson($stateParams.data);
},
modules: function (securityService) {
return securityService.getAllModules();
}
}
})
Also, I'm passing 'data' parameter as json object to the state.
Now when i first load this state, everything is fine.
But when i do browser refresh (F5 key) the $stateParams.data is null in the resolve function of the state.
How can I solve this?
I see these possible solutions:
1. persist somehow parameters
2. override browser refresh (don't know how) to stop the browser refreshing the app.
3. on refresh goto other sibling state.
Please help
UPDATE
Ok, I set data like this:
vm.editRole = function(roleId){
var role = dataService.getRoleById(roleId).then(function(result){
$state.go('roleEdit', {data:angular.toJson(result)});
});
}
UPDATE 2
The roleEdit Controller looks like this:
(function(){
angular.module('app.administration').controller('roleEdit',
['role','modules', '$scope', 'securityService', '$state', roleEditCtrl]);
function roleEditCtrl('role', 'modules',$scope, securityService, $state){
var vm = this;
vm.roles = roles;
vm.originalRole = angular.copy(role);
vm.modules=modules;
vm.saveChanges = _saveChanges;
vm.cancel = _cancel;
return vm;
function _saveChanges(){
securityService.UpdateRole(vm.role).then(function(result){
$staste.go('^.roles');
}
}
function _cancel(){
vm.role = angular.copy(vm.originalRole);
$sscope.roleEditForm.$setPristine();
}
}
})();

Had the same problem, leaving this here in case someone else needs it. Solved it by using localStorage.
I've set this as a part of app's run method
$rootScope.$on('$stateChangeSuccess', function (event, toState) {
localStorage.setItem('__stateName', toState.name);
});
Now, depending on your app's structure you might want to consider putting this someplace else, but in my case I had parent AppController surrounding all child controllers, so this part of the code went there.
var previousState = localStorage.getItem('__stateName');
previousState && $state.go(previousState) || $state.go(<SOME_OTHER_ROUTE>);
Basically, when user hits browser refresh, AppController get initialized from the start (before child controllers), firing the state transition immediately if there was one. Otherwise, you'd go to some other state of yours.

I would stay away from option 2. You don't want to mess up a browser's expected behavior.
Maybe you could refactor your data into your resolve object ala:
resolve: {
role: function ($stateParams) {
return angular.fromJson($stateParams.data);
},
modules: function (securityService) {
return securityService.getAllModules();
},
data: function (dataService) {
return dataService.retrieveStoredData();
}
Where your dataService would be a service you use to store that data if it really is that important (through cookies or localstorage).
There is no reasonable way to expect that field to be populated on browser refresh if you previously executed javascript to pass stateful values to it.

Related

AngularJS proper way of refreshing data in Controller

I have a testing "hello" view showing "Hello {{username}}!" or "Hello anonymous!".
This view has its own controller and is accesible via url (configure by ui.router).
Then I have a UserModel with methods setUsername(newUsername) and getUsername().
There is also logging view with a controller that uses setUsername() method on logging in success and then navigates to "hello" view.
The code looks like this:
HelloController:
anguler.module('hello', ...
.config(function($stateProvider){
$stateProvider
.state('hello', {
url: '/hello',
views: {
'main#': {
controller: 'HelloController as helloController',
templateUrl: 'app/hello/hello-tmpl.html'
}
},
});
})
.controller('HelloController', function (UserModel) {
var helloController = this;
helloController.username = UserModel.getUsername();
})
There is also a "log out" button in the top bar. So in order to show the changes in "hello" view I added a list of function that UserModel would call when user state changes:
.service('UserModel', function() {
var model = this;
var username = '';
var onChangesFunctions = [];
function onChange() {
onChangesFunctions.forEach(function(f) {
f();
});
}
model.onChange = function(f) {
onChangesFunctions.push(f);
};
model.setUsername = function(newUsername) {
username = newUsername;
onChange();
};
model.clearUserData = function() {
username = '';
onChange();
};
and added a code in HelloController to add a listener to the UserModel.onChangesFunctions.
The problem with that approach is that HelloController is initialized many times (everytime that user navigates to it) and every time it is registering new function as the listener.
Is there any better way to refresh user data?
The problem of your approach is memory leaks. As you said when your controller is destroyed and the new one is created your function will still persist in the service and the controller which should have been killed is still most likely in the memory because of the function.
I don't clearly understand what your goal is; however what you can do is destroying the functions when the controller is destroyed:
.controller('HelloController', function (UserModel, $scope) {
var helloController = this;
helloController.username = UserModel.getUsername();
$scope.$on('$destroy', function() {
// either destroy all functions you add to the service queue
// or
// simply call the controller specific logic here, this will be called when your controller is destroyed
});
});
Another approach is listening on '$stateChangeStart' / '$stateChangeSuccess' event.
Regardless of the way you choose I would highly recommend to avoid binding services to the controller instance specific logic. This is a way to hell

angular resolve dependency on promise

So here's my general problem, I have a factory which needs to make an ajax call to get its value. I want to inject this factory, but only after the ajax call has been resolved, meaning, when it's injected, I want the value to already be there. Is this possible? Let me give you some context so you can maybe give me a better option.
I have a UserFactory.
var app = angular.module('app',[]);
app.factory('UserFactory',[
function(){
//ajax call to get the user object
//dont allow injection until user has arrived
}
]);
var home = angular.module('home',[]);
home.controller('UserInfoController',[
'UserFactory',//I don't want this resolved until the ajax call has completed
function(User){
//bind user data to the view
}
]);
Is this possible? Or am I going about this wrong? I want to be able to access the user's information without having to use promises every time, just in case it's not already there.
What you can do is set up your factory so that it remembers the user data.
Something like this
app.factory('userFactory', function ($http){
return {
getUser: function(){
return $http({
method: 'GET',
url: ''
});
},
currentUser: {}
};
});
So then in your controller, check to see if there is anything in currentUser. If not make the getUser call and store it into currentUser. Since the factory is a singleton the userdata will always exists inside this factory and you can inject it anywhere you need it.
Here's an example: http://jsfiddle.net/4m6gmsw2/
If you still need the user data to be there before injecting it you can take a look at using resolve: https://thinkster.io/egghead/resolve/
In the state router, there is resolve property for each state.
.state({
name: 'myState',
resolve: {
user: function(myService) {
return myService.asyncGet();
}
}
Then in your controller:
.controller('myController', function(user) {
$scope.myData = user.data; //no messy promises
});

ui-router - Default substate based on data

I have a list of modules with module detail on the right side.
div
ul
li(data-ng-repeat="module in modules")
a(ng-href="{{getModuleHref(module)}}")
div(ui-view)
Both module list and each module has it's own state. I want the parent state to be addressable, therefore no abstract.
.state('modules', {
url: '/',
templateUrl: ..
})
.state('modules.input', {
url: '/input',
templateUrl: ..
})
...
When accessing the list without explicitly selected module, I want to have selected first failed module. (redirect to another state based on downloaded data)
I've tried several paths without success:
a) state.onEnter - is not triggered when redirecting from child state (e.g. the back button or manual url change) and $state.current is not target state yet (so I do not know if we aren't in the child state already)
b) redirect in $urlRouterProvider - absence of downloaded data and I'm unable to download it (sync vs async)
c) global $stateChangeSuccess handling .. unability to access the data (even if it should be already downloaded via state.resolve) - so this one is probably managable at the cost of getting data multiple times.
So I've been left with changing state in controller's constructor and registering custom $stateChangeSuccess inside a controller to handle the back button/manual url edit.
function ModulesController($scope, apiService, $state, $rootScope) {
var _this = this;
this.scope = $scope;
this.getInitialJobTraceDetail = function () {
apiService.getModules(function (modules) {
_this.scope.modules = modules;
_this.setDefaultState();
});
};
this.setDefaultState = function () {
if (_this.$state.current.name != 'modules')
return;
var targetModule = _this.getFailedModule();
_this.$state.go(targetModule);
};
this.setupModulesStateWatch = function () {
_this.registerOnStateChangeEvent();
_this.scope.$on("$destroy", function () {
_this.unregisterOnStateChangeEvent();
});
};
this.registerOnStateChangeEvent = function () {
_this.unregisterOnStateChangeEvent = _this.$rootScope.$on('$stateChangeSuccess', function (event, toState) {
if (toState.name == 'modules') {
_this.setDefaultState();
}
});
this.getInitialJobTraceDetail();
this.setupJobDetailStateWatch();
}
Edit: But I think there should be a better solution. Working with state inside a controller doesn't feel right - my other state-hadling code is in global separate file. Maybe that is the problem and state-handling/definitions should be with corresponding components?

Maintain Angular $scope on navigation

I have an Angular app with several controllers. I would like each controller to maintain its state even when the user navigates to another controller and back again. Essentially I want each view to load up just as the user last saw it (unless they navigate away from the app entirely or refresh etc.).
I understand how to share state between controllers using a service but this is not what I want. I could create a new service for every controller or put everything on the $rootScope but it seems wrong.
What is the recommended way to do this?
I would store any persistent data in the $cookieStore and maybe even encapsulate it in a service to avoid remembering what the interface to the cookie actually is. Here's an example:
var app = angular.module('app', [
'ngRoute',
'ngCookies'
]);
app.service('ticketService', function ($cookieStore) {
this.getTicket = function () {
var ticket = $cookieStore.get('currentTicket');
// Check server for ticket? (additional business logic)
return ticket;
};
this.setTicket = function (ticket) {
// Validate correct ticket? (additional business logic)
$cookieStore.set('currentTicket', ticket);
};
});
app.controller('ticketController', function ($scope, ticketService) {
var defaultTicket = {
name: '',
description: ''
};
$scope.ticket = ticketService.getTicket() || defaultTicket;
$scope.$watch('ticket', function (oldValue, newValue) {
if (oldValue === newValue) { return; }
ticketService.setTicket(newValue);
});
});

How do I store a current user context in AngularJS?

I have an AuthService, which logs in a user, it returns back a user json object. What I want to do is set that object and have all the changes reflected across the application (logged in/logged out state) without having to refresh the page.
How would I accomplish this with AngularJS?
The easiest way to accomplish this is by using a service. For example:
app.factory( 'AuthService', function() {
var currentUser;
return {
login: function() { ... },
logout: function() { ... },
isLoggedIn: function() { ... },
currentUser: function() { return currentUser; }
...
};
});
You can then reference this in any of your controllers. The following code watches for changes in a value from the service (by calling the function specified) and then syncs the changed values to the scope.
app.controller( 'MainCtrl', function( $scope, AuthService ) {
$scope.$watch( AuthService.isLoggedIn, function ( isLoggedIn ) {
$scope.isLoggedIn = isLoggedIn;
$scope.currentUser = AuthService.currentUser();
});
});
And then, of course, you can use that information however you see fit; e.g. in directives, in templates, etc. You can repeat this (customized to what you need to do) in your menu controllers, etc. It will all be updated automatically when you change the state on the service.
Anything more specific depends on your implementation.
I would amend the good response of Josh by adding that, as an AuthService is typically of interest of anyone (say, anyone but the login view should disappear if nobody is logged), maybe a simpler alternative would be to notify interested parties using $rootScope.$broadcast('loginStatusChanged', isLoggedIn); (1) (2), while interested parties (such as controllers) would listen using $scope.$on('loginStatusChanged', function (event, isLoggedIn) { $scope.isLoggedIn = isLoggedIn; }.
(1) $rootScope being injected as an argument of the service
(2) Note that, in the likely case of a asynchronous login operation, you'll want to notify Angular that the broadcast will change things, by including it in a $rootScope.$apply() function.
Now, speaking of keeping the user context in every/many controllers, you might not be happy listening for login changes in everyone of them, and might prefer to listen only in a topmost login controller, then adding other login-aware controllers as children/embedded controllers of this one. This way, the children controller will be able to see the inherited parent $scope properties such as your user context.

Resources