¿Cómo hago para que el botón Atrás funcione con una máquina de estado de enrutador ui de AngularJS?

87

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?

biofractal
fuente
1
"los estados no se identifican mediante direcciones URL", y sospecho que existe su problema. El botón de retroceso está bastante protegido del código (de lo contrario, la gente lo anularía y causaría problemas). ¿Por qué no dejar que angular haga mejores URL, como lo hace SO (de acuerdo, puede que no estén usando angular, pero su esquema de URL es ilustrativo)?
jcollum
Además, esta pregunta puede ayudar: stackoverflow.com/questions/13499040/…
jcollum
Además, dado que no está utilizando URL, ¿no significa eso que para llegar al estado Z, las personas tendrán que hacer clic en el estado X e Y para acceder a él? Eso puede resultar molesto.
jcollum
¿Va a ir con el estado con diferentes parámetros? @jcollum
VijayVishnu
No tengo idea, esto fue hace demasiado tiempo
jcollum

Respuestas:

78

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-routermá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 sessionStorageobjeto 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 StateHistoryServiceocupa 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 StateLocationServicemaneja 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, StateHistoryServiceentonces 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 StateHistoryServiceclave 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 urlal stateChangemétodo activará el $locationChangeSuccessevento y, por tanto, llamará al locationChangemétodo. Igualmente, llamar al @state.gofrom locationChangeactivará el $stateChangeSuccessevento y, por lo tanto, el stateChangemétodo. Esto se vuelve muy confuso y estropea el historial del navegador sin fin.

La solución es muy simple. Puede ver que la preventCallmatriz se usa como una pila ( popy push). 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 HistoryServicemétodos en el momento apropiado en el ciclo de vida de la transición de estado. Esto se hace en el .runmé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.

biofractal
fuente
1
en el ejemplo de SessionService anterior, creo que el método de acceso: debería usar @setStoragey en @getStoragelugar de '@ get / setSession'.
Peter Whitfield
3
Qué idioma se usa para este ejemplo. No parece ser angular con lo que estoy familiarizado.
deepak
El lenguaje era javascript, la sintaxis era cifrada.
biofractal
1
@jlguenego Tiene un historial de navegación funcional / botones de ida y vuelta de Bowser y URL que puede marcar.
Torsten Barthel
3
@jlguenego: las respuestas que sugieren el uso de variaciones de $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.
biofractal
46
app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) {
  $rootScope.goBack = function(){
    $window.history.back();
  }
}]);

<a href="#" ng-click="goBack()">Back</a>
Guillaume Massé
fuente
2
¡me gusta esto! ... lol ... para mayor claridad no $window.history.backestá 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.
Benjamin Conant
@BenjaminConant Para las personas que no saben cómo implementar esto, simplemente coloque la $window.history.back();función en una función para llamarla ng-click.
chakeda
rootScope correcto es solo hacer que la función sea accesible en cualquier plantilla
Guillaume Massé
Cena de pollo.
Cody
23

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

<a onclick="history.back()>Back</a>

// 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:

Buscar -> Ver -> Editar

A :

Buscar -> Ver

Bueno, eso es lo que queríamos, excepto que si ahora hace clic en el botón Atrás del navegador, estará allí nuevamente:

Buscar -> Ver -> Editar

Y eso no es lógico

Sin embargo, usando la solución simple

<a onclick="history.back()"> Back </a>

desde :

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

Se respeta la coherencia. :-)

bArraxas
fuente
7

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.

dgrant069
fuente
3

solución del botón de avance / retroceso del navegador
Encontré el mismo problema y lo resolví usando el popstate eventobjeto $ window y ui-router's $state object. Un evento popstate se envía a la ventana cada vez que cambia la entrada activa del historial.
Los eventos $stateChangeSuccessy $locationChangeSuccessno 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 maina folderque mainotra vez, cuando se pulse backen el navegador, podrá volver a la folderruta. La ruta se actualiza, pero la vista no y aún muestra lo que tenga 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 de retroceso / avance deberían funcionar sin problemas después de eso.

nota: compruebe la compatibilidad del navegador para window.onpopstate () para asegurarse

jfab fabuloso
fuente
3

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.

hthetiot
fuente
0

El botón Atrás tampoco me funcionaba, pero descubrí que el problema era que tenía htmlcontenido dentro de mi página principal, en el ui-viewelemento.

es decir

<div ui-view>
     <h1> Hey Kids! </h1>
     <!-- More content -->
</div>

Así que moví el contenido a un nuevo .htmlarchivo y lo marqué como una plantilla en el .jsarchivo con las rutas.

es decir

   .state("parent.mystuff", {
        url: "/mystuff",
        controller: 'myStuffCtrl',
        templateUrl: "myStuff.html"
    })
CodyBugstein
fuente
-1

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 defaultStatees el nombre del estado que se usa cuando no hay un estado anterior 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;
                });
        }]);
Sel
fuente