Angular states - resolve before exiting - angularjs

My Angular app includes a main screen and several tabs and I'm using $stateProvider to switch between states. Whenever I'm leaving a tab (=state) I must perform an asynchronous operation. I would like to wait for that operation to complete before actually moving to the new tab. Somewhat like resolve that is done when leaving the state rather than entering.
How can this be achieved?
Example snippet:
$stateProvider
.state('myproject.main', {
url: '/main',
templateUrl: '...',
controller: 'MainCtrl'
})
.state('myproject.tabs.a', {
url: '/tab_a',
controller: 'TabACtrl',
templateUrl: '...',
onExit: doSave
})
.state('myproject.tabs.b', {
url: '/tab_b',
controller: 'TabBCtrl',
templateUrl: '...',
onExit: doSave
});
function doSave(MyService) {
return MyService.save(); //async, promise
}
Here I wanted to save (async) whenever switching between the tabs. However when entering the main state from a tab, I want to be sure that save has finished (doing the save on myproject.main's onEnter/resolve is not good for me).
Problem: when entering myproject.main (MainCtrl), the async MyService.save() operation is still processing. I want it to be completed/resolved.
I tried to hack it with the following piece of code, but it does not work - the state change enters a loop
.run(function($rootScope, $state, $stateParams, MyService){
$rootScope.$on('$stateChangeStart', function(ev, toState, toParams, fromState, fromParams){
if (toState.name === 'myproject.main' && //switching to main
fromState.name && //for a certain tab
fromParams.resume !== true ) {
ev.preventDefault(); //stop the state change
//do the save and then continue with the state change
MyService.save().then(function(){
$state.go(toState, {resume: true}) //continue with a "resume" parameter
});
}
});
});
Thanks!

Related

Function inside parent state resolve isn't firing

Last few days I've been struggling with AngularJS ui-router. I am trying to fire function on every route change, but it doesn't seem to work.
I have routes like this:
.state('app', {
url: '/app',
abstract: true,
templateUrl: 'pages/menu.html',
resolve: {
function() {
console.log('test');
}
}
})
.state('app.home', {
url: '/home',
views: {
'menuContent': {
templateUrl: 'pages/home.html',
controller: 'homeCtrl'
}
}
})
.state('app.profile', {
url: '/profile',
views: {
'menuContent': {
templateUrl: 'pages/profile.html',
controller: 'profileCtrl'
}
}
})
Why that function inside parent state isn't firing when changing route from app.home to app.profile or vice versa?
Inside menu.html I navigate to state directly with href="#/app/home
I believe the parent controller code is being ran once when the app is first loaded.
If you want to something to run every time you change a state (I assume that's what you mean by route change), you can use an event like $stateChangeSuccess inside the parent controller.
Here's an example:
$scope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
if (toState.name === 'app.about') {
console.log('This is the about state!');
}
}
You could call the function you want inside this listener and then it should fire every time.

How to redirect to root if param is not found? Angular ui-router

I have the following routes, using Angular ui-router.
If I navigate to /3872 that gives me a user with that ID and the correct template, that's great.
But the problem is, if I type an ID that doesn't exist, such as /1111 I want to redirect to the root. At the moment it stays on the user route and gives me an empty template instead.
$stateProvider
.state('root', {
url: '/',
templateUrl: 'home.html',
controller: 'home_controller'
})
.state('user', {
url: '/:userId',
templateUrl: 'user.html',
controller: 'user_controller'
});
$urlRouterProvider.otherwise('/');
One way is to use $stateChangeStart event to make some filtering. That goes into app.run(...) section
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
if (toState.name == "user") {
//some logic... and most probably
//event.preventDefault();
//$state.go(toState.name, toParams);
}
});
In user_controller($stateParams,$state):
if($stateParams.userId not in userIds) {
$state.go('root');
}

$state.go timing out within $stateChangeStart

I'm currently trying to check to see if a user is authenticated when the state changes. In my run block I'm listening for $stateChangeStart event. In the following code I can currently get the console to log podio is authenticated and podio is not authenticated, but when i add $state.go into the catch statement, it loads infinitely and then times out. I've tried adding 'event.preventDefault` as it says here, but I don't fully understand why that would be necessary.
Run Block
$rootScope.$on('$stateChangeStart', function (event) {
...
Podio.podio.isAuthenticated()
.then(function () {
console.log('podio is authenticated');
})
.catch(function(error) {
console.log('podio is not authenticated');
event.preventDefault();
$state.go('login'); //when this is added it continues to load...
});
...
});
I'm sure it's not the state definitions, because they've been working correctly all along.
State Definitions
$stateProvider
// setup an abstract state for the tabs directive
.state('tab', {...})
.state('tab.events', {...})
.state('tab.event-detail',{...})
.state('tab.attendees',{...})
.state('tab.attendee-detail',{...})
.state('login', {
url: '/login',
templateUrl: 'views/login.html',
controller: 'LoginController'
});
You run isAuthenticated check on each stateChangeStart. If this check fails you end up in catch block navigating to some state. This triggers stateChangeStart again with failing authentication check.
You can add some flag to login state to allow access for unauthenticated users:
.state('login', {
url: '/login',
requireAuthentication: false,
templateUrl: 'views/login.html',
controller: 'LoginController'
})
And check it later:
$rootScope.$on('$stateChangeStart', function (event, toState) {
...
if(!toState.hasOwnProperty('requireAuthentication') || toState.requireAuthentication !== false)
Podio.podio.isAuthenticated()
...
}

initial $stateParams empty?

Plunker:
I am using UI-Router for a simple single page application. It is a just a table that you can sort and filter. When you apply a filter, like ordering a column in ascending order, the URL updates to something like:
www.domain.com#/column/'column name'/desc/false/
I then grab the paramters using $stateParams, and update the table accordingly.
$scope.$on('$stateChangeSuccess', function (evt, toState, toParams, fromState, fromParams) {
$scope.sorting.predicate = toParams.column;
$scope.sorting.reverse = boolVal(toParams.sort);
});
However, if I were to visit www.domain.com#/column/'column name'/desc/false/,
the table doesn't load with the data filtered. I am trying the following (after injecting $stateParams to my controller):
$scope.$on('$viewContentLoaded', function (event, viewConfig) {
console.log('VCL stateParams: ' + angular.toJson($stateParams));
});
However, stateParams is always empty regardless of the URL (only on viewContentLoaded). I can't seem to get the parameters of the URL on page load. Am I doing something incorrectly?
Here is the state config:
app.config(['$stateProvider', '$urlRouterProvider',
function ($stateProvider, $urlRouterProvider) {
$stateProvider.state("name", {
url: '/column/:column/desc/:sort/',
controller: 'Ctrl',
resolve: {
logSomeParams: ['$stateParams', '$state', function ($stateParams, $state) {
console.log($stateParams);
console.log(this); // this is the state you transitioned From.
console.log($state); // this is the state you transitioned To.
}]
}
});
$urlRouterProvider.otherwise('/');
}
]);
Here is how the state change is called:
<a ui-sref="name({column:'\''+column+'\'',sort:sorting.reverse})"
ng-click="sorting.predicate = '\'' + column + '\''; sorting.reverse=!sorting.reverse;">
{{column}}</a>
Here's a few options I'd investigate further.
Option 1: $rootScope listener
$scope.$root.$on('$stateChangeSuccess', function (evt, toState, toParams, fromState, fromParams) {
$scope.sorting.predicate = toParams.column;
$scope.sorting.reverse = boolVal(toParams.sort);
});
$scope.$root.$on('$viewContentLoaded', function (event, viewConfig) {
console.log('VCL stateParams: ' + angular.toJson($stateParams));
});
Option 2: Resolve objects
In your state definition, try to add this:
resolve: {
logSomeParams: ['$stateParams', '$state', function ($stateParams, $state) {
console.log($stateParams);
console.log(this); // this is the state you transitioned From.
console.log($state); // this is the state you transitioned To.
}]
}
Please give either of those a go and let me know what results you get.
The resolve object should run prior to $viewContentLoaded, but since all you are really interested in are the stateParams in your $viewContentLoaded callback, might aswell put it in the state definition.
Good luck,
Kasper.
Edit:
Try giving your state a parent state with the resolve object.
Like this:
$stateProvider.state('parentState', {
url: '',
abstract: true,
resolve: {
logSomeParams: ['$stateParams', '$state', function ($stateParams, $state) {
console.log($stateParams);
console.log(this); // this is the state you transitioned From.
console.log($state); // this is the state you transitioned To.
}]
}
});
$stateProvider.state("name", {
url: '/column/:column/desc/:sort/',
parent: 'parentState',
controller: 'Ctrl'
});
Let me know how that works out for ya.
Figured it out, quite unfortunate how simple the answer was. I needed to inject $state. I only injected $stateParams. You need both. Hope this saves someone some hair pulling and screaming.

Conditional Page Display when application opens for the first time

Hi I am just starting to learn angular && angular-ui-router and am trying to figure out how to determine when the app is opened for the first time to send the user to the login page or home page.
This is what i have so far:
codeArtApp.config(function($stateProvider, $urlRouterProvider){
$stateProvider
.state('login',{
url : '/login',
templateUrl:'App/scripts/login/loginView.html',
controller: 'loginCtrl'
})
.state('profile', {
url : '/profile',
templateUrl:'App/scripts/login/loginView.html',
controller: 'profileCtrl'
})
.state('404', {
url: '/404',
templateUrl:'App/scripts/views/404.html'
});
$urlRouterProvider.otherwise("404");
});
codeArtApp.run(['$state', '$rootScope', function($state, $rootScope, $log){
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState){
if(fromState.url === '^' && toState.url !== 'login'){
$state.go('login');
}
});
}]);
What I assumed here is that if fromState.url is set to ^ then the application is starting for the first time.
For some reason this code enters an infinite loop and I gt this stacktrace:
Now from what I can tell this happened because event if $state.go('login') gets execute toState.url is always 404.
I had hoped if $state.go('login') gets executed toState.url would have been set to login and that call would not be executed anymore.
I may not be setting this logic in the right place...
Can anyone tell me how conditionaly page displayed is achieved in Angular?
In your $urlRouterProvider.otherwise call you can pass a function instead of a string. Something like this:
$urlRouterProvider.otherwise(function($injector){
var $state = $injector.get('$state');
var Storage = $injector.get('Storage');
if (Storage.has('authToken')) {
$state.go('home');
}
else {
$state.go('login');
}
});
Hope this helps!
There is a link to working plunker, the main changes are shown in the code:
codeArtApp.run(['$state', '$rootScope',
function($state, $rootScope, $log) {
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState) {
// instead of
// toState.url !== 'login'
// we compare
// toState.name !== 'login'
var shouldLogin = fromState.url === '^'
&& toState.name !== 'login';
if(shouldLogin)
{
// this way we stop current execution
event.preventDefault();
// and navigate to login
$state.go('login');
}
});
}
]);
The toState.url would contain the url, i.e. '/login' instead of 'login'. Also check the : State Change Events to see more about the event.preventDefault(); . The plunker showing the above...

Resources