Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

stop angular-ui-router navigation until promise is resolved

I want to prevent some flickering that happens when rails devise timeout occurs, but angular doesn't know until the next authorization error from a resource.

What happens is that the template is rendered, some ajax calls for resources happen and then we are redirected to rails devise to login. I would rather do a ping to rails on every state change and if rails session has expired then I will immediately redirect BEFORE the template is rendered.

ui-router has resolve that can be put on every route but that doesn't seem DRY at all.

What I have is this. But the promise is not resolved until the state is already transitioned.

$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){         //check that user is logged in         $http.get('/api/ping').success(function(data){           if (data.signed_in) {             $scope.signedIn = true;           } else {             window.location.href = '/rails/devise/login_path'           }         })      }); 

How can I interrupt the state transition, before the new template is rendered, based on the result of a promise?

like image 558
Homan Avatar asked Nov 20 '13 11:11

Homan


2 Answers

I know this is extremely late to the game, but I wanted to throw my opinion out there and discuss what I believe is an excellent way to "pause" a state change. Per the documentation of angular-ui-router, any member of the "resolve" object of the state that is a promise must be resolved before the state is finished loading. So my functional (albeit not yet cleaned and perfected) solution, is to add a promise to the resolve object of the "toState" on "$stateChangeStart":

for example:

$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {     toState.resolve.promise = [         '$q',         function($q) {             var defer = $q.defer();             $http.makeSomeAPICallOrWhatever().then(function (resp) {                 if(resp = thisOrThat) {                     doSomeThingsHere();                     defer.resolve();                 } else {                     doOtherThingsHere();                     defer.resolve();                 }             });             return defer.promise;         }     ] }); 

This will ensure that the state-change holds for the promise to be resolved which is done when the API call finishes and all the decisions based on the return from the API are made. I've used this to check login statuses on the server-side before allowing a new page to be navigated to. When the API call resolves I either use "event.preventDefault()" to stop the original navigation and then route to the login page (surrounding the whole block of code with an if state.name != "login") or allow the user to continue by simply resolving the deferred promise instead of trying to using bypass booleans and preventDefault().

Although I'm sure the original poster has long since figured out their issue, I really hope this helps someone else out there.

EDIT

I figured I didn't want to mislead people. Here's what the code should look like if you are not sure if your states have resolve objects:

$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {     if (!toState.resolve) { toState.resolve = {} };     toState.resolve.pauseStateChange = [         '$q',         function($q) {             var defer = $q.defer();             $http.makeSomeAPICallOrWhatever().then(function (resp) {                 if(resp = thisOrThat) {                     doSomeThingsHere();                     defer.resolve();                 } else {                     doOtherThingsHere();                     defer.resolve();                 }             });             return defer.promise;         }     ] }); 

EDIT 2

in order to get this working for states that don't have a resolve definition you need to add this in the app.config:

   var $delegate = $stateProvider.state;         $stateProvider.state = function(name, definition) {             if (!definition.resolve) {                 definition.resolve = {};             }              return $delegate.apply(this, arguments);         }; 

doing if (!toState.resolve) { toState.resolve = {} }; in stateChangeStart doesn't seem to work, i think ui-router doesn't accept a resolve dict after it has been initialised.

like image 143
Joe.Flanigan Avatar answered Sep 30 '22 02:09

Joe.Flanigan


I believe you are looking for event.preventDefault()

Note: Use event.preventDefault() to prevent the transition from happening.

$scope.$on('$stateChangeStart',  function(event, toState, toParams, fromState, fromParams){          event.preventDefault();          // transitionTo() promise will be rejected with          // a 'transition prevented' error }) 

Although I would probably use resolve in state config as @charlietfl suggested

EDIT:

so I had a chance to use preventDefault() in state change event, and here is what I did:

.run(function($rootScope,$state,$timeout) {  $rootScope.$on('$stateChangeStart',     function(event, toState, toParams, fromState, fromParams){          // check if user is set         if(!$rootScope.u_id && toState.name !== 'signin'){               event.preventDefault();              // if not delayed you will get race conditions as $apply is in progress             $timeout(function(){                 event.currentScope.$apply(function() {                     $state.go("signin")                 });             },300)         } else {             // do smth else         }     } )  } 

EDIT

Newer documentation includes an example of how one should user sync() to continue after preventDefault was invoked, but exaple provided there uses $locationChangeSuccess event which for me and commenters does not work, instead use $stateChangeStart as in the example below, taken from docs with an updated event:

angular.module('app', ['ui.router'])     .run(function($rootScope, $urlRouter) {         $rootScope.$on('$stateChangeStart', function(evt) {             // Halt state change from even starting             evt.preventDefault();             // Perform custom logic             var meetsRequirement = ...             // Continue with the update and state transition if logic allows             if (meetsRequirement) $urlRouter.sync();         });     }); 
like image 40
Ivar Avatar answered Sep 30 '22 04:09

Ivar