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.
Related
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();
};
});
}]);
I use angular's ui-router and nested routes in a project and I'm faced by the problem that everything works like a charm when I use links (ui-sref) to navigate to the user's detail page with the userId as part of the url. When I refresh the page, state params are missing.
So I've taken a look at the angular demo: http://angular-ui.github.io/ui-router/sample/#/contacts/1/item/b and couldn't reproduce this behaviour, however nested states are not part of this demo in contrast to my application.
$stateProvider
.state('base', {
abstract: true
})
.state('home', {
parent: 'base',
url: '/',
views: {
'content#': {
templateUrl: 'app/index/index.view.html',
controller: 'IndexController',
controllerAs: 'home'
}
}
})
.state('users', {
url: '/users',
parent: 'base',
abstract: true
})
.state('users.list', {
url: '/list',
views: {
'content#': {
templateUrl: 'app/users/users.list.html',
controller: 'UsersController',
controllerAs: 'users'
}
},
permissions: {
authorizedRoles: UserRoles.ALL_ROLES
}
})
.state('users.details', {
url: '/:userId/details',
views: {
'content#': {
templateUrl: 'app/users/user.details.html',
controller: 'UserDetailsController',
controllerAs: 'userDetails'
}
},
resolve: {
logSomeParams: ['$stateParams', '$state', function ($stateParams, $state) {
console.log($stateParams);
console.log(this);
console.log($state);
}]
}
})
When refreshing the page the url immediately changes to http://localhost:3000/#/users//details and console output (resolve function) shows that params are missing.
html5Mode (LocationProvider) is not enabled. I already found "solutions" like redirecting back to the list if the the userId is missing on page refresh, but I just can't believe that there isn't a better way to solve this.
This is how I linked the details page in the overview (and it is working):
<div class="panel-body" ui-sref="users.details({userId: user.siloUserId})">
As expected, my problem had nothing to do with the ui-router. The URLMatcher works as expected, even if you refresh the page (everything else would have been a huge dissapointment).
However, I have a $stateChangeStart listener which checks if the SAML authentication session (cookie) is still valid and waits for the result.
function(event, toState, toParams, fromState, fromParams){
if(!authenticationService.isInitialized()) {
event.preventDefault();
authenticationService.getDeferred().then(() => {
$state.go(toState);
});
} else {
//check authorization
if ( authenticationService.protectedState(toState) && !authenticationService.isAuthorized(toState.permissions.authorizedRoles)) {
authenticationService.denyAccess();
event.preventDefault();
}
}
}
No idea how this could happen, but I forgot to pass the parameters to the go method of the stateProvider. So all I had to change was to add the params:
$state.go(toState, toParams);
I have code like this
<a ui-sref="nested.something">something</a>
<div ui-view="nested.something"></div>
how to load ui-view without click ui-sref ?
EXTEND - related to this plunker provided by OP in the comments above
The state definition is:
.state('store', {
views: {
'store': {
templateUrl: 'store.html'
}
}
})
.state('store.detail', {
views: {
'store_detail': {
templateUrl: 'store_detail.html'
}
}
})
Then in this updated plunker we can see that this would do the job
//$urlRouterProvider.otherwise('/store');
$urlRouterProvider.otherwise(function($injector, $location){
var state = $injector.get('$state');
state.go('store.detail');
return $location.path();
});
Reason? states do not have defined url. Which is a bit weird. So, I would honestly rather suggested to do it like this (the link to such plunker):
$urlRouterProvider.otherwise('/store/detail');
//$urlRouterProvider.otherwise(function($injector, $location){
// var state = $injector.get('$state');
// state.go('store.detail');
// return $location.path();
//});
$stateProvider
.state('store', {
url: '/store',
views: {
'store': {
templateUrl: 'store.html'
}
}
})
.state('store.detail', {
url: '/detail',
views: {
'store_detail': {
templateUrl: 'store_detail.html'
}
}
})
There is a working plunker
ORIGINAL
We can use the .otherwise(rule) of $urlRouterProvider, documented here
$urlRouterProvider.otherwise('/parent/child');
As the doc says:
otherwise(rule)
Defines a path that is used when an invalid route is requested.
So, this could be used for some default - start up "redirection"
The .otherwise() could be even a function, like shown here:
How not to change url when show 404 error page with ui-router
which takes '$injector', '$location' and can do even much more magic (on invalid or startup path)
$urlRouterProvider.otherwise(function($injector, $location){
var state = $injector.get('$state');
state.go('404');
return $location.path();
});
ALSO, if we want to fill in some more details into some nested viesw, we can do it by defining multi-named views:
.state('parent.child', {
url: "/child",
views: {
'' : {
templateUrl: 'tpl.child.html',
controller: 'ChildCtrl',
},
'nested.something#parent.child' : {
templateUrl: 'tpl.something.html',
},
}
})
So, if the tpl.child.html will have this anchor/target:
<i>place for nested something:</i>
<div ui-view="nested.something"></div>
it will be filled with the tpl.something.html content
Check it in action here
I am asking a similar question to this question: UI Router conditional ui views?, but my situation is a little more complex and I cannot seem to get the provided answer to work.
Basically, I have a url that can be rendered two very different ways, depending on the type of entity that the url points to.
Here is what I am currently trying
$stateProvider
.state('home', {
url : '/{id}',
resolve: {
entity: function($stateParams, RestService) {
return RestService.getEntity($stateParams.id);
}
},
template: 'Home Template <ui-view></ui-view>',
onEnter: function($state, entity) {
if (entity.Type == 'first') {
$state.transitionTo('home.first');
} else {
$state.transitionTo('home.second');
}
}
})
.state('home.first', {
url: '',
templateUrl: 'first.html',
controller: 'FirstController'
})
.state('home.second', {
url: '',
templateUrl: 'second.html',
controller: 'SecondController'
});
I set up a Resolve to fetch the actual entity from a restful service.
Every thing seems to be working until I actually get to the transitionTo based on the type.
The transition seems to work, except the resolve re-fires and the getEntity fails because the id is null.
I've tried to send the id to the transitionTo calls, but then it still tries to do a second resolve, meaning the entity is fetched from the rest service twice.
What seems to be happening is that in the onEnter handler, the state hasn't actually changed yet, so when the transition happens, it thinks it is transitioning to a whole new state rather than to a child state. This is further evidenced because when I remove the entity. from the state name in the transitionTo, it believes the current state is root, rather than home. This also prevents me from using 'go' instead of transitionTo.
Any ideas?
The templateUrl can be a function as well so you check the type and return a different view and define the controller in the view rather than as part of the state configuration. You cannot inject parameters to templateUrl so you might have to use templateProvider.
$stateProvider.state('home', {
templateProvider: ['$stateParams', 'restService' , function ($stateParams, restService) {
restService.getEntity($stateParams.id).then(function(entity) {
if (entity.Type == 'first') {
return '<div ng-include="first.html"></div>;
} else {
return '<div ng-include="second.html"></div>';
}
});
}]
})
You can also do the following :
$stateProvider
.state('home', {
url : '/{id}',
resolve: {
entity: function($stateParams, RestService) {
return RestService.getEntity($stateParams.id);
}
},
template: 'Home Template <ui-view></ui-view>',
onEnter: function($state, entity) {
if (entity.Type == 'first') {
$timeout(function() {
$state.go('home.first');
}, 0);
} else {
$timeout(function() {
$state.go('home.second');
}, 0);
}
}
})
.state('home.first', {
url: '',
templateUrl: 'first.html',
controller: 'FirstController'
})
.state('home.second', {
url: '',
templateUrl: 'second.html',
controller: 'SecondController'
});
I ended up making the home controller a sibling of first and second, rather than a parent, and then had the controller of home do a $state.go to first or second depending on the results of the resolve.
Use verified code for conditional view in ui-route
$stateProvider.state('dashboard.home', {
url: '/dashboard',
controller: 'MainCtrl',
// templateUrl: $rootScope.active_admin_template,
templateProvider: ['$stateParams', '$templateRequest','$rootScope', function ($stateParams, templateRequest,$rootScope) {
var templateUrl ='';
if ($rootScope.current_user.role == 'MANAGER'){
templateUrl ='views/manager_portal/dashboard.html';
}else{
templateUrl ='views/dashboard/home.html';
}
return templateRequest(templateUrl);
}]
});
I'm showing and hiding a modal when the user go to a specific state (/login), but I would keep the modal in background if the users goes to /register from /login.
If the user is in /login and then goes to /register, I would the login modal to stay opened, while if the user is in /login and then goes to a different page, I would the login modal to disappear.
Actually I set the Angular-ui-router $stateProvider in this way:
app.config(function ($stateProvider, $urlRouterProvider, $locationProvider) {
$stateProvider
.state('home', {
url: "/",
templateUrl: "templates/home.html",
controller: 'HomeCtrl'
})
.state('login', {
url: "/login",
onEnter: function ($stateParams, $state, Modal) {
// * modalCtrl for Login
if (window.loginModal) {
window.loginModal.remove();
delete window.loginModal;
Modal.fromTemplateUrl('templates/user/login.html', function (modal) {
window.loginModal = modal;
loginModal.show();
});
} else {
Modal.fromTemplateUrl('templates/user/login.html', function (modal) {
window.loginModal = modal;
loginModal.show();
});
}
},
onExit: function ($stateParams, $state) {
if ( window.loginModal && /* condition like "!nextState.is('register')" */ ) {
window.loginModal.hide();
}
}
})
.state('register', {
url: "/register",
onEnter: function ($stateParams, $state, Modal, SlideBoxDelegate) {
// * modalCtrl for Register
if (window.registerModal) {
window.registerModal.remove();
delete window.registerModal;
Modal.fromTemplateUrl('templates/user/register.html', function (modal) {
window.registerModal = modal;
SlideBoxDelegate.update();
registerModal.show();
});
} else {
Modal.fromTemplateUrl('templates/user/register.html', function (modal) {
window.registerModal = modal;
SlideBoxDelegate.update();
registerModal.show();
});
}
},
onExit: function ($stateParams, $state) {
if ( window.registerModal ) {
window.registerModal.hide();
}
}
})
.state('not_found', {
url: "/not_found",
templateUrl: 'templates/not_found.html',
controller: 'NotFoundCtrl'
})
$urlRouterProvider.otherwise("/not_found");
$locationProvider.html5Mode(true);
})
Is there a way to set the condition like "!nextState.is('register')"?
Thank you for reading :)
You can listen for $stateChangeStart and note the value of the to parameter some place that is accessible to your onExit function, i.e. an injectable value or service.
In the future, there will be an injectable service that will represent the transition itself, which you'll be able to inspect and manipulate for things like this.
The best trick I have found so far is to use the promise inside the $state service, in the onExit function :
onExit: function ($state) {
$state.transition.then(toState => {
if (toState.name === 'nameOfTheWantedNextState') {
$state.go($state.current, {}, {reload: true});
}
})
}
This avoids listening to the event and storing the name somewhere, it only uses whats available at the time of the transition. Quite neat !