angularjs - example - Detenga la navegación del enrutador angular-ui hasta que se cumpla la promesa
ui-router params (10)
Quiero evitar algunos parpadeos que ocurren cuando se producen tiempos de espera de los rieles, pero angular no sabe hasta el siguiente error de autorización de un recurso.
Lo que sucede es que la plantilla se procesa, algunas llamadas ajax de recursos suceden y luego se nos redirige a los rieles para que inicien sesión. Preferiría hacer un ping a los rieles en cada cambio de estado y si la sesión de los rieles ha expirado, redirigiré inmediatamente ANTES de que se renderice la plantilla.
ui-router tiene una resolución que puede colocarse en cada ruta pero que no parece estar SECA en absoluto.
Lo que tengo es esto. Pero la promesa no se resuelve hasta que el estado ya está en transición.
$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''
}
})
});
¿Cómo puedo interrumpir la transición de estado, antes de que se renderice la nueva plantilla, en función del resultado de una promesa?
Aquí está mi solución a este problema. Funciona bien, y está en el espíritu de algunas de las otras respuestas aquí. Se acaba de limpiar un poco. Estoy configurando una variable personalizada llamada ''stateChangeBypass'' en el ámbito raíz para evitar un bucle infinito. También estoy comprobando si el estado es ''iniciar sesión'' y, de ser así, siempre está permitido.
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'');
}
});
});
}
Como $ urlRouter.sync () no funciona con stateChangeStart, aquí hay una alternativa:
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
}
});
Creo que estás buscando event.preventDefault()
Nota: use event.preventDefault () para evitar que se produzca la transición.
$scope.$on(''$stateChangeStart'',
function(event, toState, toParams, fromState, fromParams){
event.preventDefault();
// transitionTo() promise will be rejected with
// a ''transition prevented'' error
})
Aunque probablemente usaría resolve
en la configuración del estado como sugirió @charlietfl
EDITAR :
así que tuve la oportunidad de usar preventDefault () en un evento de cambio de estado, y esto es lo que hice:
.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
}
}
)
}
EDITAR
La documentación más reciente incluye un ejemplo de cómo se debe continuar un usuario sync()
para continuar después de que se haya invocado preventDefault
, pero siempre que se use el evento $locationChangeSuccess
que para mí y los comentaristas no funciona, en su lugar, use $stateChangeStart
como se $stateChangeStart
en el siguiente ejemplo, tomado de documentos con un evento actualizado:
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();
});
});
El método on
devuelve a deregistration function for this listener
.
Así que aquí está lo que puedes hacer:
var unbindStateChangeEvent = $scope.$on(''$stateChangeStart'',
function(event, toState, toParams) {
event.preventDefault();
waitForSomething(function (everythingIsFine) {
if(everythingIsFine) {
unbindStateChangeEvent();
$state.go(toState, toParams);
}
});
});
Me encontré con el mismo problema Resuelto mediante el uso de este.
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
})
});
Para agregar a las respuestas existentes aquí, tuve exactamente el mismo problema; estábamos usando un controlador de eventos en el ámbito raíz para escuchar a $stateChangeStart
para mi manejo de permisos. Desafortunadamente, esto tuvo el efecto secundario desagradable de ocasionar infinitos compendios (no tengo idea de por qué, el código no fue escrito por mi)
La solución que se me ocurrió, que es bastante deficiente, es evitar siempre la transición con event.preventDefault()
, luego determinar si el usuario ha iniciado sesión a través de una llamada asíncrona. Luego de verificar esto, use $state.go
para pasar a un nuevo estado. Sin embargo, lo importante es que establece la propiedad notify
en las opciones en $state.go
en falso. Esto evitará que las transiciones de estado $stateChangeStart
otro $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 });
});
Sin embargo, esto no es muy deseable, pero es necesario para mí debido a la forma en que se cargan los permisos en este sistema; si hubiera usado un permiso de hasPermission
sincrónico, los permisos podrían no haberse cargado en el momento de la solicitud a la página. :( ¿Podríamos pedirle a ui-router un método de Transición continueTransition
en el evento?
authSvc.hasPermissionAsync(toState.data.permission).then(continueTransition).catch(function() {
cancelTransition();
return $state.go(''login'', {}, { notify: false });
});
Puede capturar los parámetros de transición de $ stateChangeStart y guardarlos en un servicio, luego reiniciar la transición después de que haya tratado con el inicio de sesión. También puede consultar https://github.com/witoldsz/angular-http-auth si su seguridad proviene del servidor como errores http 401.
Realmente me gusta la solución sugerida por TheRyBerg , ya que puedes hacerlo todo en un solo lugar y sin demasiados trucos extraños. Descubrí que hay una manera de mejorarlo aún más, para que no necesites el stateChangeBypass en el rootscope. La idea principal es que desea tener algo inicializado en su código antes de que su aplicación pueda "ejecutarse". Luego, si recuerdas si está inicializado o no, puedes hacerlo de esta manera:
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);
});
}
});
Entonces puede recordar que sus datos ya están inicializados en el servicio de la forma que desee, por ejemplo:
function dataService() {
var initialized = false;
return {
initialize: initialize,
isInitialized: isInitialized
}
function intialize() {
return $http.get(...)
.success(function(response) {
initialized=true;
});
}
function isInitialized() {
return initialized;
}
};
Sé que esto es muy tarde para el juego, pero quería expresar mi opinión y discutir lo que creo que es una excelente manera de "pausar" un cambio de estado. Según la documentación de angular-ui-router, cualquier miembro del objeto "resolver" del estado que es una promesa debe resolverse antes de que el estado termine de cargarse. Por lo tanto, mi solución funcional (aunque aún no se ha limpiado y perfeccionado) es agregar una promesa al objeto de resolución de "toState" en "$ stateChangeStart":
por ejemplo:
$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;
}
]
});
Esto asegurará que el cambio de estado se cumple para que se cumpla la promesa que se realizará cuando finalice la llamada a la API y se tomen todas las decisiones basadas en el retorno de la API. He usado esto para verificar los estados de inicio de sesión en el lado del servidor antes de permitir que se navegue por una nueva página. Cuando se resuelve la llamada a la API, o bien uso "event.preventDefault ()" para detener la navegación original y luego la ruta a la página de inicio de sesión (que rodea todo el bloque de código con un if state.name! = "Login") o permitir que el usuario para continuar simplemente resolviendo la promesa diferida en lugar de tratar de usar Booleans de bypass y preventDefault ().
Aunque estoy seguro de que el póster original hace tiempo que resolvió su problema, realmente espero que esto ayude a alguien más.
EDITAR
Pensé que no quería engañar a la gente. Este es el aspecto que debe tener el código si no está seguro de que sus estados hayan resuelto los objetos:
$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;
}
]
});
Editar 2
para que funcione para los estados que no tienen una definición de resolución, debe agregar esto en el archivo app.config:
var $delegate = $stateProvider.state;
$stateProvider.state = function(name, definition) {
if (!definition.resolve) {
definition.resolve = {};
}
return $delegate.apply(this, arguments);
};
haciendo if (!toState.resolve) { toState.resolve = {} };
en stateChangeStart no parece funcionar, creo que ui-router no acepta un dictado de resolución después de que se haya inicializado.
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);
});
});
Tuve algunos problemas con el uso de un guardia booleano para evitar el bucle infinito durante el estado de cambio de estado, por lo que adopté este enfoque de solo verificar si se intentó la misma transición nuevamente y regresar inmediatamente si es así, ya que para ese caso la promesa aún no se ha resuelto.