Implementé una aplicación de una sola página angularjs usando ui-router .
Originalmente identifiqué cada estado usando una URL distinta, sin embargo, esto hizo que las URL empaquetadas con GUID no fueran amigables.
Así que ahora he definido mi sitio como una máquina de estado mucho más simple. Los estados no se identifican mediante direcciones URL, sino que simplemente se transfieren según sea necesario, así:
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 URL, el botón de retroceso no funciona.
¿Cómo mantengo mi máquina de estado de ui-router ordenada pero habilito la funcionalidad del botón Atrás?
javascript
angularjs
coffeescript
angular-ui-router
biofractal
fuente
fuente
Respuestas:
Nota
Las respuestas que sugieren el uso de variaciones de
$window.history.back()
han pasado por alto 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 (atrás / adelante / actualizar). Con eso en mente; por favor, sigue leyendo.Sí, es posible hacer retroceder / avanzar el navegador (historial) y actualizar mientras se ejecuta una
ui-router
máquina de estado puro , pero requiere un poco de trabajo.Necesita varios componentes:
URL únicas . El navegador solo habilita los botones de retroceso / avance 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 del estado después de que su aplicación angular se haya reiniciado mediante retroceso / avance o clics de actualización.
Una historia del estado . Un diccionario simple de estados de ui-router codificados por una URL única. 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 (ver más abajo).
Un servicio de localización . Finalmente, debe poder administrar los cambios de estado de ui-router, activados internamente por su código, y los cambios normales de la URL del navegador que normalmente se activan cuando el usuario hace clic en los botones del navegador o escribe cosas en la barra del navegador. Todo esto puede volverse un poco complicado porque es fácil confundirse acerca de qué desencadenó qué.
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
sessionStorage
objeto javascript . 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 página única 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
Se
StateHistoryService
ocupa del almacenamiento y recuperación de estados históricos codificados por URL únicas generadas. En realidad, es solo un contenedor de conveniencia para un objeto de estilo de diccionario.El servicio de ubicación estatal
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
El
StateLocationService
maneja dos eventos:locationChange . Esto se llama cuando se cambia la ubicación del navegador, 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 escribe una URL. Si existe un estado para la location.url actual en el,
StateHistoryService
entonces se usa para restaurar el estado a través de ui-router$state.go
.stateChange . Esto se llama cuando mueve el estado internamente. El nombre y los parámetros del estado actual se almacenan en la
StateHistoryService
clave mediante 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 (vea el pie para el fragmento del 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 de historial interno del navegador mediante@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 llamar
@location.url url
alstateChange
método activará el$locationChangeSuccess
evento y, por tanto, llamará allocationChange
método. Igualmente, llamar al@state.go
fromlocationChange
activará el$stateChangeSuccess
evento y, por lo tanto, elstateChange
método. Esto se vuelve muy confuso y estropea el historial del navegador sin fin.La solución es muy simple. Puede ver que la
preventCall
matriz se usa como una pila (pop
ypush
). Cada vez que se llama a uno de los métodos, se evita que el otro método se llame una sola vez. Esta técnica no interfiere con la activación correcta de los eventos $ y mantiene todo en orden.Ahora todo lo que tenemos que hacer es llamar a los
HistoryService
métodos en el momento apropiado en el ciclo de vida de la transición de estado. Esto se hace en el.run
método de AngularJS Apps , así:Ejecución de aplicación angular
angular .module 'app', ['ui.router'] .run ($rootScope, stateLocationService) -> $rootScope.$on '$stateChangeSuccess', (event, toState, toParams) -> stateLocationService.stateChange() $rootScope.$on '$locationChangeSuccess', -> stateLocationService.locationChange()
Generar una guía
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 como se esperaba.
fuente
@setStorage
y en@getStorage
lugar de '@ get / setSession'.$window.history.back()
han pasado por alto una parte crucial de la pregunta. El objetivo era restaurar el estado de la aplicación a la ubicación de estado correcta a medida que el historial salta (retroceder / avanzar / actualizar). Normalmente, esto se lograría proporcionando los datos de estado a través del URI. La pregunta preguntaba cómo saltar entre ubicaciones de estado sin datos de estado de URI (explícitos). Dada esta restricción, no es suficiente simplemente replicar el botón de retroceso porque esto depende de los datos de estado de URI para restablecer la ubicación del estado.app.run(['$window', '$rootScope', function ($window , $rootScope) { $rootScope.goBack = function(){ $window.history.back(); } }]); <a href="#" ng-click="goBack()">Back</a>
fuente
$window.history.back
está haciendo la magia$rootScope
... así que volver podría estar vinculado al alcance de la directiva de la barra de navegación si lo desea.$window.history.back();
función en una función para llamarlang-click
.Después de probar diferentes propuestas, descubrí que la forma más fácil suele ser la mejor.
Si usa un enrutador ui angular y necesita un botón para retroceder mejor es este:
<button onclick="history.back()">Back</button>
o
// Advertencia: no establezca el href o la ruta se romperá.
Explicación: suponga una aplicación de gestión estándar. Buscar objeto -> Ver objeto -> Editar objeto
Usando las soluciones angulares de este estado:
A :
Bueno, eso es lo que queríamos, excepto que si ahora hace clic en el botón Atrás del navegador, estará allí nuevamente:
Y eso no es lógico
Sin embargo, usando la solución simple
<a onclick="history.back()"> Back </a>
desde :
después de hacer clic en el botón:
después de hacer clic en el botón Atrás del navegador:
Se respeta la coherencia. :-)
fuente
Si está buscando el botón "atrás" más simple, puede 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 "atrás" bastante poco inteligente porque utiliza el historial del navegador. Si lo incluye en su página de destino, enviará al usuario de regreso a cualquier URL de la que provenga antes de llegar a la suya.
fuente
solución del botón de avance / retroceso del navegador
Encontré el mismo problema y lo resolví usando el
popstate event
objeto $ window yui-router's $state object
. Un evento popstate se envía a la ventana cada vez que cambia la entrada activa del historial.Los eventos
$stateChangeSuccess
y$locationChangeSuccess
no se activan al hacer clic en el botón del navegador, aunque la barra de direcciones indique la nueva ubicación.Así, suponiendo que ha navegado de los estados
main
afolder
quemain
otra vez, cuando se pulseback
en el navegador, podrá volver a lafolder
ruta. La ruta se actualiza, pero la vista no y aún muestra lo que tengamain
. 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 de retroceso / avance deberían funcionar sin problemas después de eso.
nota: compruebe la compatibilidad del navegador para window.onpopstate () para asegurarse
fuente
Se puede resolver usando una directiva simple "go-back-history", esta también cierra la ventana en caso de que no haya historial previo.
Uso de directivas
<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: Funciona con ui-router o no.
fuente
El botón Atrás tampoco me funcionaba, pero descubrí que el problema era que tenía
html
contenido dentro de mi página principal, en elui-view
elemento.es decir
<div ui-view> <h1> Hey Kids! </h1> <!-- More content --> </div>
Así que moví el contenido a un nuevo
.html
archivo y lo marqué como una plantilla en el.js
archivo con las rutas.es decir
.state("parent.mystuff", { url: "/mystuff", controller: 'myStuffCtrl', templateUrl: "myStuff.html" })
fuente
history.back()
y cambiar al estado anterior a menudo dan el efecto no deseado. Por ejemplo, si tiene un formulario con pestañas y cada pestaña tiene su propio estado, esto simplemente cambió la pestaña anterior seleccionada, no regresa del formulario. En el caso de estados anidados, por lo general es necesario, así que piense en los estados principales que desea deshacer.Esta directiva resuelve 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 mostrara el botón. Opcional
defaultState
es el nombre del estado que se usa cuando no hay un estado anterior en la memoria. También restaura la posición de desplazamientoCó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; }); }]);
fuente