web-dev-qa-db-fra.com

arrêter la navigation angular-ui-router jusqu'à ce que la promesse soit résolue

Je souhaite éviter tout scintillement lors de l'expiration du délai d'attente de conception de Rails, mais angular ne le sait pas avant la prochaine erreur d'autorisation d'une ressource.

Ce qui se passe, c'est que le modèle est rendu, que des appels ajax de ressources se produisent et que nous sommes redirigés vers le système Rails pour nous connecter. Je préférerais faire un ping sur Rails à chaque changement d'état et si la session Rails a expiré, je vais immédiatement rediriger AVANT que le modèle soit rendu.

le routeur ui a une résolution qui peut être mise sur chaque route, mais cela ne semble pas du tout DRY.

Ce que j'ai est ceci. Mais la promesse n'est pas résolue tant que l'état n'est pas déjà passé. 

$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'
          }
        })

    });

Comment puis-je interrompre la transition d'état, avant le rendu du nouveau modèle, en fonction du résultat d'une promesse?

45
Homan

Je sais que c'est très tard pour le jeu, mais je voulais exprimer mon opinion et discuter de ce que je considère être un excellent moyen de "suspendre" un changement d'état. Selon la documentation de angular-ui-router, tout membre de l'objet "résoudre" de l'état faisant l'objet d'une promesse doit être résolu avant le chargement complet de l'état. Donc, ma solution fonctionnelle (bien que pas encore nettoyée et perfectionnée), est d’ajouter une promesse à l’objet de résolution du "toState" sur "$ stateChangeStart":

par exemple:

$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;
        }
    ]
});

Cela garantira que le changement d'état est valable pour la promesse à résoudre qui est effectuée lorsque l'appel de l'API est terminé et que toutes les décisions basées sur le retour de l'API sont prises. Je l'ai utilisé pour vérifier les statuts de connexion côté serveur avant de permettre la navigation vers une nouvelle page. Lorsque l'appel d'API est résolu, j'utilise soit "event.preventDefault ()" pour arrêter la navigation d'origine, puis me diriger vers la page de connexion (entourant tout le bloc de code avec un if state.name! = "Login") continuer en résolvant simplement la promesse différée au lieu d'essayer d'utiliser contournement booleans et preventDefault ().

Bien que je sois sûr que l'affiche originale ait compris depuis longtemps son problème, j'espère vraiment que cela aidera quelqu'un d'autre.

MODIFIER

J'ai pensé que je ne voulais pas induire les gens en erreur. Voici à quoi devrait ressembler le code si vous n'êtes pas sûr que vos états contiennent des objets de résolution:

$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

pour que cela fonctionne pour les états qui n'ont pas de définition de résolution, vous devez ajouter ceci dans le fichier app.config:

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

            return $delegate.apply(this, arguments);
        };

faire if (!toState.resolve) { toState.resolve = {} }; dans stateChangeStart ne semble pas fonctionner, je pense que ui-router n'accepte pas un dict de résolution après son initialisation.

46
Joe.Flanigan

Je crois que vous recherchez event.preventDefault()

Remarque: Utilisez event.preventDefault () pour empêcher la transition.

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

Bien que j'utiliserais probablement resolve dans l'état de configuration comme @charlietfl le suggérait

MODIFIER:

j'ai donc eu l'occasion d'utiliser preventDefault () dans un événement de changement d'état, et voici ce que j'ai fait:

.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
        }
    }
)

}

MODIFIER

Documentation plus récente inclut un exemple de la manière dont l'utilisateur sync() doit continuer après que preventDefault a été appelé, mais exaple utilise un événement $locationChangeSuccess qui, pour moi et les commentateurs ne fonctionne pas, utilise plutôt $stateChangeStart comme dans l'exemple ci-dessous, tiré de docs avec un événement mis à jour:

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();
        });
    });
27

Voici ma solution à ce problème. Cela fonctionne bien et correspond à l’esprit de certaines des autres réponses données ici. C'est juste nettoyé un peu. Je suis en train de définir une variable personnalisée appelée "stateChangeBypass" sur la portée racine pour empêcher les boucles infinies. Je vérifie également si l'état est 'login' et si c'est le cas, c'est toujours autorisé.

function ($rootScope, $state, Auth) {

    $rootScope.$on('$stateChangeStart', function (event, toState, toParams) {

        if($rootScope.stateChangeBypass || toState.name === 'login') {
            $rootScope.stateChangeBypass = false;
            return;
        }

        event.preventDefault();

        Auth.getCurrentUser().then(function(user) {
            if (user) {
                $rootScope.stateChangeBypass = true;
                $state.go(toState, toParams);
            } else {
                $state.go('login');
            }
        });

    });
}
24
rmberg

comme $ urlRouter.sync () ne fonctionne pas avec stateChangeStart, voici une alternative:

    var bypass;
    $rootScope.$on('$stateChangeStart', function(event,toState,toParams) {
        if (bypass) return;
        event.preventDefault(); // Halt state change from even starting
        var meetsRequirement = ... // Perform custom logic
        if (meetsRequirement) {  // Continue with the update and state transition if logic allows
            bypass = true;  // bypass next call
            $state.go(toState, toParams); // Continue with the initial state change
        }
    });
15
oori

La méthode on renvoie a deregistration function for this listener.

Alors voici ce que vous pouvez faire:

var unbindStateChangeEvent = $scope.$on('$stateChangeStart', 
  function(event, toState, toParams) { 
    event.preventDefault(); 

    waitForSomething(function (everythingIsFine) {
      if(everythingIsFine) {
        unbindStateChangeEvent();
        $state.go(toState, toParams);
      }
    });
});
4
Yann Bertrand

Pour ajouter aux réponses existantes ici, j'avais exactement le même problème; nous utilisions un gestionnaire d'événements sur l'étendue racine pour écouter $stateChangeStart pour le traitement de mes autorisations. Malheureusement, cela a eu un vilain effet secondaire de provoquer parfois des digests infinis (je ne sais pas pourquoi, le code n'a pas été écrit par moi).

La solution que je propose, qui fait plutôt défaut, est de toujours empêcher la transition avec event.preventDefault(), puis de déterminer si l'utilisateur est connecté ou non via un appel asynchrone. Après vérification, utilisez $state.go pour passer à un nouvel état. Le bit important, cependant, est que vous définissez la propriété notify sur les options de $state.go sur false. Cela empêchera les transitions d'état de déclencher un autre $stateChangeStart.

 event.preventDefault();
 return authSvc.hasPermissionAsync(toState.data.permission)
    .then(function () {
      // notify: false prevents the event from being rebroadcast, this will prevent us
      // from having an infinite loop
      $state.go(toState, toParams, { notify: false });
    })
    .catch(function () {
      $state.go('login', {}, { notify: false });
    });

Ce n’est pas très souhaitable cependant, mais c’est nécessaire pour moi en raison de la manière dont les autorisations de ce système sont chargées; Si j'avais utilisé une variable hasPermission synchrone, les autorisations n'auraient peut-être pas été chargées au moment de la demande sur la page. :( Peut-être pourrions-nous demander à ui-router une méthode continueTransition sur l'événement?

authSvc.hasPermissionAsync(toState.data.permission).then(continueTransition).catch(function() {
  cancelTransition();
  return $state.go('login', {}, { notify: false });
});
4
Dan Pantry

J'aime beaucoup la solution suggérée par TheRyBerg , puisque vous pouvez tout faire au même endroit et sans trop d’astuces étranges. J'ai trouvé qu'il y avait un moyen de l'améliorer encore plus, de sorte que vous n'ayez pas besoin de l'étatChangeBypass dans le rootscope. L'idée principale est que vous souhaitiez que quelque chose soit initialisé dans votre code avant que votre application puisse "s'exécuter". Ensuite, si vous vous rappelez s'il est initialisé ou non, vous pouvez le faire de cette façon:

rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState) {

    if (dataService.isInitialized()) {
        proceedAsUsual(); // Do the required checks and redirects here based on the data that you can expect ready from the dataService
    } 
    else {

        event.preventDefault();

        dataService.intialize().success(function () {
                $state.go(toState, toParams);
        });
    }
});

Ensuite, vous pouvez simplement vous souvenir que vos données sont déjà initialisées dans le service comme vous le souhaitez, par exemple:

function dataService() {

    var initialized = false;

    return {
        initialize: initialize,
        isInitialized: isInitialized
    }

    function intialize() {

        return $http.get(...)
                    .success(function(response) {
                            initialized=true;
                    });

    }

    function isInitialized() {
        return initialized;
    }
};
2

Vous pouvez récupérer les paramètres de transition à partir de $ stateChangeStart et les stocker dans un service, puis réactiver la transition une fois que vous avez géré la connexion. Vous pouvez également consulter https://github.com/witoldsz/angular-http-auth si votre sécurité provient du serveur sous forme d'erreurs http 401.

1
laurelnaiad
        var lastTransition = null;
        $rootScope.$on('$stateChangeStart',
            function(event, toState, toParams, fromState, fromParams, options)  {
                // state change listener will keep getting fired while waiting for promise so if detect another call to same transition then just return immediately
                if(lastTransition === toState.name) {
                    return;
                }

                lastTransition = toState.name;

                // Don't do transition until after promise resolved
                event.preventDefault();
                return executeFunctionThatReturnsPromise(fromParams, toParams).then(function(result) {
                    $state.go(toState,toParams,options);
                });
        });

J'ai eu quelques problèmes en utilisant un garde booléen pour éviter la boucle infinie pendant stateChangeStart, donc j'ai pris cette approche de simplement vérifier si la même transition a été tentée à nouveau et de revenir immédiatement si si, dans ce cas, la promesse n'a toujours pas été résolue.

0
GameSalutes

J'ai couru dans le même problème résolu en utilisant ceci. 

angular.module('app', ['ui.router']).run(function($rootScope, $state) {
    yourpromise.then(function(resolvedVal){
        $rootScope.$on('$stateChangeStart', function(event){
           if(!resolvedVal.allow){
               event.preventDefault();
               $state.go('unauthState');
           }
        })
    }).catch(function(){
        $rootScope.$on('$stateChangeStart', function(event){
           event.preventDefault();
           $state.go('unauthState');
           //DO Something ELSE
        })

    });
0
Mohan Kethees