Angular ui-router shows old stale data after transition back to state - angularjs

We have an ASP.NET MVC 5 application which uses ui-router AngularJS module.
When we go back to a state that has already been loaded, it always shows the old data until we do a full page refresh.
For example:
User clicks 'View Profile', we show the "ViewProfile" state, a page displaying the profile
User clicks "Edit Profile", we show the "EditProfile" state, a page with fields to edit the profile
User makes changes and clicks 'Save', they are then moved back to the "ViewProfile" state
When "ViewProfile" state loads, it still shows the old data before the edits
How can we force ui-route to pull fresh data any time it loads any state?
Angular config
var app = angular.module("MyApp", ['ngIdle', 'ui.router'])
.config(['$stateProvider', '$urlRouterProvider', '$locationProvider',
function ($stateProvider, $urlRouterProvider, $locationProvider) {
// Configure client-side routing
$locationProvider.hashPrefix("!").html5Mode(true);
$urlRouterProvider.otherwise("/");
$stateProvider
.state('Home', {
url: '/',
views: {
"mainContainer": {
templateUrl: function (params) { return 'Home/Index'; }
}
}
})
.state('ProfileManagement', {
url: '/ProfileManagement-{action}',
views: {
"mainContainer": {
templateUrl: function (params) { return 'ProfileManagement/' + params.action; }
}
}
})
}]);
How we are doing the transition
$state.go(stateName, { action: actionName }, { reload: true, inherit: false, notify: true });
EDIT w/ Solution
Since all the functionality is written using jQuery, we cannot use Angular controllers to control the data. Our solution was to completely disable Angular template caching with the following code:
app.run(function ($rootScope, $templateCache) {
$rootScope.$on('$viewContentLoaded', function () {
$templateCache.removeAll();
});
});

By default ui-router state transition will not re-instantiate toState's controller.
To force instantiation of the controller, you can do this.
//assuming you are using $state.go to go back to previous state
$state.go('to.state', params, {
reload: true
});

You need to use resolves, like so:
var app = angular.module("MyApp", ['ngIdle', 'ui.router'])
.config(['$stateProvider', '$urlRouterProvider', '$locationProvider',
function ($stateProvider, $urlRouterProvider, $locationProvider) {
// Configure client-side routing
$locationProvider.hashPrefix("!").html5Mode(true);
$urlRouterProvider.otherwise("/");
$stateProvider
.state('Home', {
url: '/',
views: {
"mainContainer": {
templateUrl: function (params) { return 'Home/Index'; }
}
},
resolve: {
loadedData: (injectable) => injectable.getData()
}
})
.state('ProfileManagement', {
url: '/ProfileManagement-{action}',
views: {
"mainContainer": {
templateUrl: function (params) { return 'ProfileManagement/' + params.action; }
}
},
resolve: {
loadedData: (injectable) => injectable.getData()
}
})
}]);
In each state, there is a resolve, calling the method 'getData()' in the injectable service (I'm saying a 'service' because that's where we would have the data loaded from).
Then for each state's controller, it will have access to the loadedData resolve; the data will already be loaded at that point and be usable by that controller. So for your case scenario, going back to the state you've come from, the newly updated data will be loaded again and available in that state again.
Keep your state transition the same, with the reload: true, as that is what you want as well.

Maybe this work for your problem.
In your angular main page you should have something like this:
app.run(['$rootScope', function ($rootScope){
$rootScope.$on('$stateChangeStart', function(event, toState){
$rootScope.UrlActive=toState.name;
});
}]);
In your angular controller you should have something like this:
app.controller('AnalisisCtrl', ['$scope','$rootScope', function ($scope,$rootScope) {
$scope.$watch('UrlActive', function(newValue) {
if(newValue="ProfileManagement"){
doSomething();
};
});
}]);

Related

angular ui-router- ajax call on load

We are developing an single page application using angular JS and I am using state provider for configuring routes. Basically there is a global navigation view and a dashboard view. I have to pass few params from navigation to make a service call and then display the dashboard accordingly.I have split the states as two, one for navigation and other for dashboard. THe thing which i am not able to figure out is that where should i make ajax call to fetch dashboard data. Should i make it in navigation itself and pass it through resolve. or should i just pass the data to dashboard controller and make ajax call from there. Below is my state
$stateProvider
.state('home', {
url: '/',
templateUrl: 'templates/home.htm',
controller: 'homeController',
})
.state('dashboard', {
url: 'contact',
templateUrl: 'templates/dashboard.htm',
controller: 'dashboardController'
})
.state('state3', {
url: '/articles',
templateUrl: 'templates/state3.htm',
controller: 'state3Controller'
});
$urlRouterProvider.otherwise('/home');
This entirely depends on how you want the user experience to play out.
If you want to do all the data fetching before transitioning to the dashboard state, use a resolve state configuration
.state('dashboard', {
url: '/contact',
templateUrl: 'templates/dashboard.htm',
controller: 'dashboardController',
resolve: {
someData: function($http) {
return $http.get('something').then(res => res.data);
}
}
}
then your controller can be injected with someData, eg
.controller('dashboardController', function($scope, someData) { ... })
This will cause the state transition to wait until the someName promise has been resolved meaning the data is available right away in the controller.
If however you want to immediately transition to the dashboard state (and maybe show a loading message, spinner, etc), you would move the data fetching to the controller
.controller('dashboardController', function($scope, $http) {
$scope.loading = true; // just an example
$http.get('something').then(res => {
$scope.loading = false;
$scope.data = res.data;
});
})

angular-ui ui-router controller not getting instantiated with $state.go

I am trying to build an app which on login changes the state to a view with $state.go but when calling $state.go the controller is not instantiated as defined for that state.
Here is my state change logic (removing other code for brevity):
config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('login', {
url: "/",
templateUrl: "templates/login.html",
controller: 'LoginController'
})
// setup an abstract state for the tabs directive
.state('tab', {
url: "/tab",
abstract: true,
templateUrl: "templates/tabs.html"
})
// Each tab has its own nav history stack:
.state('tab.dash', {
url: '/dash',
views: {
'tab-dash': {
templateUrl: 'templates/tab-dash.html',
controller: 'DashboardController'
}
},
resolve: {
authenticated: ['RestService', function(restService) {
return restService.authenticationStatus();
}],
sessionService: ['SessionService', function(sessionService) {
return sessionService;
}]
}
})
My LoginController is something like:
// Perform the login action when the user submits the login form
$scope.doLogin = function(loginForm) {
if (loginForm.$invalid) {
$log.debug('Invalid Form...', $scope.loginData);
return;
}
$log.debug('Doing login', $scope.loginData);
RestService.login($scope.loginData)
.then(function(data) {
$log.debug("Inside loginController...");
$log.debug(data);
$scope.$state.go("tab.dash");
}, function(data) {
$log.debug(data);
$scope.formErrors = data.errors;
});
};
And my DashboardController is something like:
angular.module('starter.controller.dashboard', [])
.controller('DashboardController', ['$scope', '$log', '$http',
function($scope, $log, $http) {
$log.debug("reaching here..................");
$log.debug($scope.authenticated);
}]);
Now when the login succeeds, the state is transitioned to /tab/dash but the controller is not instantiated i.e. the debug logs in DashboardController are not printed. If I directly navigate to /tab/dash then the controller does get instantiated and I do see the logs getting printed.
Moreover the value of "authenticated" passed via resolve in state definition is not available via scope in templates.
Well turns out that the controller is getting instantiated but it gets instantiated only once. If I don't refresh the page while testing and just change the path then since the controller is already instantiated, it is not instantiated again. Only if I refresh the page (LoginPage) and then navigate to Dashboard page (via $state.go on logging in) the controller gets instantiated again.
And the issue with resolve data not available in the controller is because my assumption was that it is auto injected in $scope but actually it is not so. The resolve params get injected explicitly via the passed params to constructor function of controller and then one needs to assign the values manually in the scope. Something like:
.controller('DashboardController', ['$rootScope', '$scope', '$log', '$ionicLoading', '$http', 'authenticated',
function($rootScope, $scope, $log, $ionicLoading, $http, authenticated) {
$scope.authenticated = authenticated;
}]);

How to redirect users with unverified emails using Angular UI-Router?

I am using AngularJS with Meteor and wanted to redirect users with unverified emails to the sign in page. I have created a sign in view in /client/routes.js:
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/');
$stateProvider
.state('signin', {
url:'/signin',
views: {
main: {
templateUrl: 'client/views/profile/signin.tpl'
}
}
})
Note that there are other states I am not listing for brevity sake.
Now, I want to redirect users to this sign in page if their emails have not been verified. How do I modify the example below from UI-Router FAQs to meet my needs? Other solutions not using the example below are acceptable to me as long as they address the issue at hand.
Example: Uses the data object on the state config to define a rule
function that will run logic against the user (here using an example
service called $currentUser). The $stateChangeStart handler catches
all state transition and performs this rule check before allowing the
transition, potentially blocking it and/or redirecting to a different
state.
app.config(function($stateProvider) {
$stateProvider.state('privatePage', {
data: {
rule: function(user) {
// ...
}
});
});
app.run(function($rootScope, $state, $currentUser) {
$rootScope.$on('$stateChangeStart', function(e, to) {
if (!angular.isFunction(to.data.rule)) return;
var result = to.data.rule($currentUser);
if (result && result.to) {
e.preventDefault();
// Optionally set option.notify to false if you don't want
// to retrigger another $stateChangeStart event
$state.go(result.to, result.params, {notify: false});
}
});
});
The example from the FAQ attempts to create a general way to add a rule to any page. Let's keep it simple:
app.run(function($rootScope, $state, UserService) {
$rootScope.$on('$stateChangeStart', function(event, toState) {
// don't check auth on login routes
if (["signin"].indexOf(toState.name) === -1) {
if (UserService.doesNotHaveVerifiedEmail()) {
event.preventDefault();
$state.go('signin');
return;
}
}
}
});
Anytime a state is loaded and it's not the signin state, you check if the user is verified (depends on your application, here I am injecting a UserService which I assume has knowledge about the user's status) and if not, you prevent that state change and redirect them to the signin page.
You can use the resolve functionality provided by angular-ui-router to check email verification of current user before the state is resolved. Here's how the code will look like:
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
var isVerified = ['User', '$state', '$q',
function(User, $state, $q) {
var d = $q.defer();
var loginPromise = User.getVerificationStatus();
loginPromise.then(
function(response) {
d.resolve(response);
},
function(error) {
$state.go('login');
});
return d.promise;
}
];
$urlRouterProvider.otherwise('/');
$stateProvider
.state('signin', {
url: '/signin',
views: {
main: {
templateUrl: 'client/views/profile/signin.tpl'
}
}
})
.state('home', {
url: '/home',
views: {
main: {
templateUrl: 'client/views/profile/home.tpl'
}
},
resolve: {
verified: isVerified
}
});
}
]);
Here home state checks for the verification before resolving. I have injected a service User which will arrange the information whether user is verified or not.
You can add resolve property to only those states where you want to check verification status. In this way this is better than checking on $stateChangeStart event which will be fired every time state changes irrespective of whether this check is needed or not.
Here's the link to the documentation.

Redirect state in angularjs

This is the state configuration:
angular
.module('grabhutApp', [...])
.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
// ACCOUNT
.state('account', {
abstract: true,
url: '/account',
templateUrl: 'index.html'
})
.state('account.main', {
url: '',
templateUrl: 'views/account/account.login.html',
controller: 'AccountController'
})
.
.
.
// NO VIEWS
.state('nv', {
abstract: true,
url: '/nv'
})
.state('nv.logout', {
url: '/logout'
})
});
The nv and its sub states will have no physical views or controllers.
I want them to serve as links that calls certain functions.
Service for calling logout methods:
angular.module('grabhutApp')
.factory('$grabhutAccountService', function ($state, $grabhutDataService) {
var methods = {
.
.
logoutUser: function () {
$grabhutDataService.user.removeSession();
$state.go('account.main', {}, {location: 'replace'});
}
.
.
};
return methods;
});
Then a button/link for logout:
<a ui-sref="nv.logout" class="button icon icon ion-log-out button-large" menu-close></a>
What I want to happen is that, when state nv.logout was triggered the
$grabhutAccountService.logoutUser() must be called and must redirect to 'account.main'
Here is what I've done so far:
I tried to use resolve in nv.logout
.state('nv.logout', {
url: '/logout',
resolve: {
logout: function ($grabhutAccountService) {
$grabhutAccountService.logoutUser();
}
}
})
The service was called but state did not redirect. So I tried another way. I added a controller:
.state('nv.logout', {
url: '/logout',
resolve: {
logout: function ($grabhutAccountService) {
$grabhutAccountService.logoutUser();
}
},
controller: function ($scope, $state) {
$scope.$on('$stateChangeSuccess', function () {
$state.go('account.main');
});
}
})
But $stateChangeSuccess is not being fired.
So I tried to use the rootScope:
.run(function(...., $grabhutAccountService){
.
.
.
$rootScope.logout = function(){
$grabhutAccountService.logoutUser();
};
.
.
.
})
And the use it like this:
<a ng-click="$root.logout()" class="button icon icon ion-log-out button-large" menu-close></a>
This works fine. But I'm worrying since (AFAIK) rootScope loads more data which could cause slower operation.
Besides, whenever I need some kind of function like above, I would have to attach function in rootScope again.
And I think that's not a good approach. BTW, I'm building this in phonegap that's why memory usage is so important.
Ooooh you're so close. I rearranged some of your code and arrived at this:
app.run(function($rootScope, $grabhutAccountService) {
$rootScope.$on('$stateChangeSuccess', function (evt, toState) {
if (toState.name === 'nv.logout') {
$grabhutAccountService.logoutUser();
$state.go('account.main');
}
});
});
The next major version of UI-Router will have much improved hooks for doing this sort of thing.

AngularJS keep template data when switching route?

In state llantas.ordenes I have a jqGrid and other controls as well as in llantas.inventarios
so whenever I switch from #/llantas/ordenes to #/llantas/inventarios I loose the controls data and the jqGrid table is being redraw, so the question is if its possible to keep view data when switching from route to route?
This is my router.js:
ng.route(function($stateProvider, $urlRouterProvider){
// Now set up the states
$stateProvider
.state('llantas', {
url: "/Llantas",
templateUrl: "templates/llantas/index.html"
})
// Ordenes
.state('llantas.ordenes', {
url: "/ordenes",
templateUrl: "templates/llantas/ordenes/index.html",
controller: function($scope, $injector) {
require(['js/controllers/llantas/ordenes/index'], function(llantasOrdenesIndexCtrl) {
$injector.invoke(llantasOrdenesIndexCtrl, this, {'$scope': $scope});
});
}
})
// Inventarios
.state('llantas.inventarios', {
url: "/inventarios",
templateUrl: "templates/llantas/inventarios/index.html",
controller: function($scope, $injector) {
require(['js/controllers/llantas/inventarios/index'], function(llantasInventariosIndexCtrl) {
$injector.invoke(llantasInventariosIndexCtrl, this, {'$scope': $scope});
});
}
})
});
I removed the UI Router and set reloadOnSearch = false on every route.

Resources