javascript - stateprovider - ui router angularjs template
¿Cómo hago para que el Botón Atrás funcione con una máquina de estado ui-router AngularJS? (8)
Nota
Las respuestas que sugieren usar variaciones de $window.history.back()
han omitido una parte crucial de la pregunta: cómo restaurar el estado de la aplicación a la ubicación de estado correcta a medida que el historial salta (retroceder / avanzar / actualizar). Con eso en mente; por favor, sigue leyendo.
Sí, es posible tener el navegador atrás / adelante (historial) y actualizar mientras se ejecuta una máquina de estado ui-router
pura, pero lleva un poco de tiempo.
Necesitas varios componentes:
URL únicas El navegador solo habilita los botones retroceder / avanzar cuando cambia las URL, por lo que debe generar una URL única por estado visitado. Sin embargo, estas URL no necesitan contener ninguna información de estado.
Un servicio de sesión . Cada URL generada se correlaciona con un estado particular, por lo que necesita una forma de almacenar sus pares de url-estado para que pueda recuperar la información de estado después de que su aplicación angular haya sido reiniciada por atrás / adelante o actualice los clics.
Una historia del estado . Un diccionario simple de ui-enrutador estados por url único. Si puede confiar en HTML5, puede usar la API de historial de HTML5 , pero si, como yo, no puede, puede implementarla usted mismo en unas pocas líneas de código (consulte a continuación).
Un servicio de ubicación . Finalmente, debe poder administrar los cambios de estado del ui-enrutador, desencadenados internamente por su código, y los cambios normales de la URL del navegador normalmente activados por el usuario haciendo clic en los botones del navegador o escribiendo cosas en la barra del navegador. Esto puede ser un poco complicado porque es fácil confundirse sobre qué fue lo que provocó.
Aquí está mi implementación de cada uno de estos requisitos. He agrupado todo en tres servicios:
El servicio de sesión
class SessionService
setStorage:(key, value) ->
json = if value is undefined then null else JSON.stringify value
sessionStorage.setItem key, json
getStorage:(key)->
JSON.parse sessionStorage.getItem key
clear: ->
@setStorage(key, null) for key of sessionStorage
stateHistory:(value=null) ->
@accessor ''stateHistory'', value
# other properties goes here
accessor:(name, value)->
return @getStorage name unless value?
@setStorage name, value
angular
.module ''app.Services''
.service ''sessionService'', SessionService
Este es un contenedor para el objeto javascript sessionStorage
. Lo he cortado para mayor claridad aquí. Para obtener una explicación completa de esto, consulte: ¿Cómo manejo la actualización de la página con una aplicación de una sola página de AngularJS?
El Servicio de Historia del Estado
class StateHistoryService
@$inject:[''sessionService'']
constructor:(@sessionService) ->
set:(key, state)->
history = @sessionService.stateHistory() ? {}
history[key] = state
@sessionService.stateHistory history
get:(key)->
@sessionService.stateHistory()?[key]
angular
.module ''app.Services''
.service ''stateHistoryService'', StateHistoryService
El StateHistoryService
se StateHistoryService
del almacenamiento y la recuperación de estados históricos codificados por urls únicas y generadas. En realidad, es solo una envoltura de conveniencia para un objeto de estilo de diccionario.
El servicio de localización del estado
class StateLocationService
preventCall:[]
@$inject:[''$location'',''$state'', ''stateHistoryService'']
constructor:(@location, @state, @stateHistoryService) ->
locationChange: ->
return if @preventCall.pop(''locationChange'')?
entry = @stateHistoryService.get @location.url()
return unless entry?
@preventCall.push ''stateChange''
@state.go entry.name, entry.params, {location:false}
stateChange: ->
return if @preventCall.pop(''stateChange'')?
entry = {name: @state.current.name, params: @state.params}
#generate your site specific, unique url here
url = "/#{@state.params.subscriptionUrl}/#{Math.guid().substr(0,8)}"
@stateHistoryService.set url, entry
@preventCall.push ''locationChange''
@location.url url
angular
.module ''app.Services''
.service ''stateLocationService'', StateLocationService
StateLocationService
maneja dos eventos:
locationChange . Se invoca cuando se cambia la ubicación de los navegadores, generalmente cuando se presiona el botón Atrás / Adelante / Actualizar o cuando la aplicación se inicia por primera vez o cuando el usuario ingresa una url. Si existe un estado para la ubicación.url actual en
StateHistoryService
entonces se usa para restablecer el estado a través del$state.go
ui-router.stateChange . Esto se llama cuando mueve el estado internamente. El nombre y los parámetros del estado actual se almacenan en el
StateHistoryService
marcado por una url generada. Esta url generada puede ser cualquier cosa que desee, puede o no identificar el estado de una manera legible por humanos. En mi caso, estoy usando un parámetro de estado más una secuencia de dígitos generada aleatoriamente derivada de un guid (ver pie para el fragmento de generador de guid). La url generada se muestra en la barra del navegador y, lo que es más importante, se agrega a la pila del historial interno del navegador usando@location.url url
. Es agregar la url a la pila del historial del navegador que habilita los botones de avance / retroceso.
El gran problema con esta técnica es que al llamar a @location.url url
en el método stateChange
se desencadenará el evento $locationChangeSuccess
y así se llama al método locationChange
. Igualmente, al llamar a @state.go
desde locationChange
se activará el evento $stateChangeSuccess
y, por lo tanto, el método stateChange
. Esto se vuelve muy confuso y arruina el historial del navegador al final.
La solución es muy simple. Puede ver la matriz preventCall
se usa como una pila ( pop
y push
). Cada vez que se llama a uno de los métodos, evita que se llame al otro método por única vez. Esta técnica no interfiere con la activación correcta de los $ eventos y mantiene todo recto.
Ahora todo lo que tenemos que hacer es llamar a los métodos de HistoryService
en el momento apropiado en el ciclo de vida de transición del estado. Esto se hace en el método .run
AngularJS Apps, así:
Aplicación angular.run
angular
.module ''app'', [''ui.router'']
.run ($rootScope, stateLocationService) ->
$rootScope.$on ''$stateChangeSuccess'', (event, toState, toParams) ->
stateLocationService.stateChange()
$rootScope.$on ''$locationChangeSuccess'', ->
stateLocationService.locationChange()
Genera un Guid
Math.guid = ->
s4 = -> Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
"#{s4()}#{s4()}-#{s4()}-#{s4()}-#{s4()}-#{s4()}#{s4()}#{s4()}"
Con todo esto en su lugar, los botones de avance / retroceso y el botón de actualización funcionan todos como se esperaba.
Implementé una aplicación de página única angularjs usando ui-router .
Originalmente identifiqué cada estado usando una url distinta, pero esto hizo que las urls empaquetadas GUID fueran hostiles.
Así que ahora he definido mi sitio como una máquina de estado mucho más simple. Los estados no se identifican por las URL, sino que simplemente se cambian según sea necesario, de esta manera:
Definir estados anidados
angular
.module ''app'', [''ui.router'']
.config ($stateProvider) ->
$stateProvider
.state ''main'',
templateUrl: ''main.html''
controller: ''mainCtrl''
params: [''locationId'']
.state ''folder'',
templateUrl: ''folder.html''
parent: ''main''
controller: ''folderCtrl''
resolve:
folder:(apiService) -> apiService.get ''#base/folder/#locationId''
Transición a un estado definido
#The ui-sref attrib transitions to the ''folder'' state
a(ui-sref="folder({locationId:''{{folder.Id}}''})")
| {{ folder.Name }}
Este sistema funciona muy bien y me encanta su sintaxis limpia. Sin embargo, como no estoy usando las URL, el botón Atrás no funciona.
¿Cómo mantengo mi máquina de estados de ui-router ordenada pero habilito la funcionalidad de botón de retroceso?
Después de probar diferentes propuestas, descubrí que la mejor manera es a menudo la mejor.
Si usa ui-router angular y necesita un botón para regresar mejor, esto es:
<button onclick="history.back()">Back</button>
o
<a onclick="history.back()>Back</a>
// Advertencia no configure el href o la ruta se romperá.
Explicación: suponga una aplicación de gestión estándar. Objeto de búsqueda -> Ver objeto -> Editar objeto
Usando las soluciones angulares de este estado:
Buscar -> Ver -> Editar
A :
Buscar -> Ver
Bueno, eso es lo que queríamos, pero si ahora haces clic en el botón Atrás del navegador volverás a estar allí:
Buscar -> Ver -> Editar
Y eso no es lógico
Sin embargo, usando la solución simple
<a onclick="history.back()"> Back </a>
de :
Buscar -> Ver -> Editar
después de hacer clic en el botón:
Buscar -> Ver
después de hacer clic en el botón Atrás del navegador:
Buscar
La consistencia es respetada. :-)
El botón Atrás no funcionaba para mí también, pero descubrí que el problema era que tenía contenido html
dentro de mi página principal, en el elemento ui-view
.
es decir
<div ui-view>
<h1> Hey Kids! </h1>
<!-- More content -->
</div>
Así que moví el contenido a un nuevo archivo .html
y lo marqué como una plantilla en el archivo .js
con las rutas.
es decir
.state("parent.mystuff", {
url: "/mystuff",
controller: ''myStuffCtrl'',
templateUrl: "myStuff.html"
})
Puede resolverse utilizando una directiva simple "historial de retroceso", esta también cierra la ventana en caso de que no haya historial anterior.
Uso de la directiva
<a data-go-back-history>Previous State</a>
Declaración de directiva angular
.directive(''goBackHistory'', [''$window'', function ($window) {
return {
restrict: ''A'',
link: function (scope, elm, attrs) {
elm.on(''click'', function ($event) {
$event.stopPropagation();
if ($window.history.length) {
$window.history.back();
} else {
$window.close();
}
});
}
};
}])
Nota: Trabajar usando ui-router o no.
Si está buscando el botón "atrás" más simple, entonces podría configurar una directiva como esta:
.directive(''back'', function factory($window) {
return {
restrict : ''E'',
replace : true,
transclude : true,
templateUrl: ''wherever your template is located'',
link: function (scope, element, attrs) {
scope.navBack = function() {
$window.history.back();
};
}
};
});
Tenga en cuenta que este es un botón de "retroceso" bastante poco inteligente porque está usando el historial del navegador. Si lo incluye en su página de destino, enviará a un usuario a cualquier url de donde provenga antes de que aterrice en el suyo.
history.back()
y cambiar al estado anterior a menudo dan el efecto no que desee. Por ejemplo, si tiene un formulario con pestañas y cada pestaña tiene un estado propio, esto solo cambia la pestaña anterior seleccionada, no regresa del formulario. En el caso de estados anidados, generalmente necesita pensar en la bruja de los estados principales que desea revertir.
Esta directiva soluciona el problema
angular.module(''app'', [''ui-router-back''])
<span ui-back=''defaultState''> Go back </span>
Vuelve al estado, que estaba activo antes de que se muestre el botón. defaultState
opcional es el nombre de estado que se usa cuando no hay estado previo en la memoria. También restaura la posición de desplazamiento
Código
class UiBackData {
fromStateName: string;
fromParams: any;
fromStateScroll: number;
}
interface IRootScope1 extends ng.IScope {
uiBackData: UiBackData;
}
class UiBackDirective implements ng.IDirective {
uiBackDataSave: UiBackData;
constructor(private $state: angular.ui.IStateService,
private $rootScope: IRootScope1,
private $timeout: ng.ITimeoutService) {
}
link: ng.IDirectiveLinkFn = (scope, element, attrs) => {
this.uiBackDataSave = angular.copy(this.$rootScope.uiBackData);
function parseStateRef(ref, current) {
var preparsed = ref.match(/^/s*({[^}]*})/s*$/), parsed;
if (preparsed) ref = current + ''('' + preparsed[1] + '')'';
parsed = ref.replace(//n/g, " ").match(/^([^(]+?)/s*(/((.*)/))?$/);
if (!parsed || parsed.length !== 4)
throw new Error("Invalid state ref ''" + ref + "''");
let paramExpr = parsed[3] || null;
let copy = angular.copy(scope.$eval(paramExpr));
return { state: parsed[1], paramExpr: copy };
}
element.on(''click'', (e) => {
e.preventDefault();
if (this.uiBackDataSave.fromStateName)
this.$state.go(this.uiBackDataSave.fromStateName, this.uiBackDataSave.fromParams)
.then(state => {
// Override ui-router autoscroll
this.$timeout(() => {
$(window).scrollTop(this.uiBackDataSave.fromStateScroll);
}, 500, false);
});
else {
var r = parseStateRef((<any>attrs).uiBack, this.$state.current);
this.$state.go(r.state, r.paramExpr);
}
});
};
public static factory(): ng.IDirectiveFactory {
const directive = ($state, $rootScope, $timeout) =>
new UiBackDirective($state, $rootScope, $timeout);
directive.$inject = [''$state'', ''$rootScope'', ''$timeout''];
return directive;
}
}
angular.module(''ui-router-back'')
.directive(''uiBack'', UiBackDirective.factory())
.run([''$rootScope'',
($rootScope: IRootScope1) => {
$rootScope.$on(''$stateChangeSuccess'',
(event, toState, toParams, fromState, fromParams) => {
if ($rootScope.uiBackData == null)
$rootScope.uiBackData = new UiBackData();
$rootScope.uiBackData.fromStateName = fromState.name;
$rootScope.uiBackData.fromStateScroll = $(window).scrollTop();
$rootScope.uiBackData.fromParams = fromParams;
});
}]);
la solución de botón de atrás / adelante del navegador
Encontré el mismo problema y lo resolví usando el popstate event
del objeto $ window y el objeto ui-router''s $state object
. Un evento de estado emergente se envía a la ventana cada vez que cambia la entrada del historial activo.
Los $stateChangeSuccess
y $locationChangeSuccess
no se activan en el clic del navegador, aunque la barra de direcciones indica la nueva ubicación.
Por lo tanto, suponiendo que haya navegado de estados main
a folder
a main
nuevamente, cuando back
presionar en el navegador, debe volver a la ruta de la folder
. La ruta se actualiza pero la vista no es y todavía muestra lo que tenga en main
. prueba esto:
angular
.module ''app'', [''ui.router'']
.run($state, $window) {
$window.onpopstate = function(event) {
var stateName = $state.current.name,
pathname = $window.location.pathname.split(''/'')[1],
routeParams = {}; // i.e.- $state.params
console.log($state.current.name, pathname); // ''main'', ''folder''
if ($state.current.name.indexOf(pathname) === -1) {
// Optionally set option.notify to false if you don''t want
// to retrigger another $stateChangeStart event
$state.go(
$state.current.name,
routeParams,
{reload:true, notify: false}
);
}
};
}
los botones atrás / adelante deberían funcionar sin problemas después de eso.
Nota: compruebe la compatibilidad del navegador para window.onpopstate () para estar seguro
app.run([''$window'', ''$rootScope'',
function ($window , $rootScope) {
$rootScope.goBack = function(){
$window.history.back();
}
}]);
<a href="#" ng-click="goBack()">Back</a>