Come faccio a far funzionare il pulsante Indietro con una macchina a stati ui-router AngularJS?


87

Ho implementato un'applicazione angularjs a pagina singola utilizzando ui-router .

Inizialmente ho identificato ogni stato utilizzando un URL distinto, tuttavia questo ha reso gli URL non amichevoli e con GUID.

Quindi ora ho definito il mio sito come una macchina a stati molto più semplice. Gli stati non vengono identificati dagli URL ma vengono semplicemente trasferiti a come richiesto, in questo modo:

Definisci stati annidati

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'

Transizione a uno stato definito

#The ui-sref attrib transitions to the 'folder' state

a(ui-sref="folder({locationId:'{{folder.Id}}'})")
    | {{ folder.Name }}

Questo sistema funziona molto bene e adoro la sua sintassi pulita. Tuttavia, poiché non sto utilizzando gli URL, il pulsante Indietro non funziona.

Come faccio a mantenere pulita la mia macchina a stati ui-router ma abilito la funzionalità del pulsante Indietro?


1
"gli stati non sono identificati dagli URL" - e sospetto che ci sia il tuo problema. Il pulsante Indietro è abbastanza protetto dal codice (altrimenti le persone lo sovrascriverebbero, causando problemi). Perché non lasciare che angular crei URL migliori, come fa SO (OK, potrebbero non usare angular, ma il loro schema URL è illustrativo)?
jcollum

Inoltre, questa domanda può aiutare: stackoverflow.com/questions/13499040/...
jcollum

Inoltre, dal momento che non stai utilizzando URL, ciò non significa che per arrivare allo stato Z le persone dovranno fare clic sullo stato X e Y per accedervi? Potrebbe diventare fastidioso.
jcollum

andrà con lo stato con parametri diversi? @jcollum
VijayVishnu

Non ne ho idea, è stato troppo tempo fa
jcollum

Risposte:


78

Nota

Le risposte che suggeriscono l'utilizzo di variazioni di $window.history.back()hanno tutte perso una parte cruciale della domanda: come ripristinare lo stato dell'applicazione nella posizione dello stato corretta quando la cronologia salta (indietro / avanti / aggiorna). Con quello in mente; per favore, continua a leggere.


Sì, è possibile fare in modo che il browser avanti / indietro (cronologia) e si aggiorni durante l'esecuzione di una ui-routermacchina a stati puri , ma ci vuole un po 'di tempo.

Hai bisogno di diversi componenti:

  • URL univoci . Il browser abilita i pulsanti Indietro / Avanti solo quando si modificano gli URL, quindi è necessario generare un URL univoco per stato visitato. Tuttavia, questi URL non devono contenere alcuna informazione di stato.

  • Un servizio di sessione . Ogni URL generato è correlato a uno stato particolare, quindi è necessario un modo per memorizzare le coppie di stato dell'URL in modo da poter recuperare le informazioni sullo stato dopo che la tua app angolare è stata riavviata con clic indietro / avanti o di aggiornamento.

  • Una storia di stato . Un semplice dizionario degli stati ui-router con chiave da un URL univoco. Se puoi fare affidamento su HTML5, puoi utilizzare l' API cronologia HTML5 , ma se, come me, non puoi, puoi implementarlo da solo in poche righe di codice (vedi sotto).

  • Un servizio di localizzazione . Infine, è necessario essere in grado di gestire sia le modifiche allo stato dell'interfaccia utente-router, attivate internamente dal codice, sia le normali modifiche all'URL del browser tipicamente attivate dall'utente che fa clic sui pulsanti del browser o digita elementi nella barra del browser. Tutto questo può diventare un po 'complicato perché è facile confondersi su ciò che ha attivato cosa.

Ecco la mia implementazione di ciascuno di questi requisiti. Ho raggruppato tutto in tre servizi:

Il servizio di sessione

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

Questo è un wrapper per l' sessionStorageoggetto javascript . L'ho tagliato per chiarezza qui. Per una spiegazione completa di ciò, vedere: Come gestire l'aggiornamento della pagina con un'applicazione AngularJS a pagina singola

Il servizio di storia dello stato

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

Si StateHistoryServiceoccupa della memorizzazione e del recupero degli stati storici codificati da URL univoci generati. In realtà è solo un comodo wrapper per un oggetto in stile dizionario.

Il servizio di localizzazione statale

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

La StateLocationServicegestisce due eventi:

  • locationChange . Viene chiamato quando la posizione del browser viene modificata, in genere quando viene premuto il pulsante Indietro / Avanti / Aggiorna o quando l'app viene avviata per la prima volta o quando l'utente digita un URL. Se uno stato per l'attuale location.url esiste in StateHistoryServiceallora viene utilizzato per ripristinare lo stato tramite ui-router $state.go.

  • stateChange . Questo viene chiamato quando si sposta lo stato internamente. Il nome e i parametri dello stato corrente vengono memorizzati nel StateHistoryServicecodice da un URL generato. Questo URL generato può essere qualsiasi cosa tu voglia, può o meno identificare lo stato in modo leggibile dall'uomo. Nel mio caso sto usando un parametro di stato più una sequenza di cifre generata casualmente derivata da un guid (vedi il piede per lo snippet del generatore di guid). L'URL generato viene visualizzato nella barra del browser e, soprattutto, aggiunto allo stack della cronologia interna del browser utilizzando @location.url url. Aggiunge l'URL allo stack della cronologia del browser che abilita i pulsanti avanti / indietro.

Il grosso problema con questa tecnica è che la chiamata @location.url urlnel stateChangemetodo attiverà l' $locationChangeSuccessevento e quindi chiamerà il locationChangemetodo. Allo stesso modo chiamando il @state.gofrom locationChangeattiverà l' $stateChangeSuccessevento e quindi il stateChangemetodo. Questo diventa molto confuso e incasina la cronologia del browser senza fine.

La soluzione è molto semplice. Puoi vedere l' preventCallarray utilizzato come stack ( pope push). Ogni volta che viene chiamato uno dei metodi, si impedisce che l'altro metodo venga chiamato una sola volta. Questa tecnica non interferisce con il corretto innesco degli eventi $ e mantiene tutto in ordine.

Ora tutto ciò che dobbiamo fare è chiamare i HistoryServicemetodi al momento opportuno nel ciclo di vita della transizione di stato. Questo viene fatto nel .runmetodo AngularJS Apps , in questo modo:

App.run angolare

angular
.module 'app', ['ui.router']
.run ($rootScope, stateLocationService) ->

    $rootScope.$on '$stateChangeSuccess', (event, toState, toParams) ->
        stateLocationService.stateChange()

    $rootScope.$on '$locationChangeSuccess', ->
        stateLocationService.locationChange()

Genera una guida

Math.guid = ->
    s4 = -> Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
    "#{s4()}#{s4()}-#{s4()}-#{s4()}-#{s4()}-#{s4()}#{s4()}#{s4()}"

Con tutto ciò a posto, i pulsanti avanti / indietro e il pulsante di aggiornamento funzionano tutti come previsto.


1
Nell'esempio di SessionService sopra, penso che il metodo accessor: dovrebbe usare @setStoragee @getStorageinvece di `@ get / setSession '.
Peter Whitfield,

3
Quale lingua viene utilizzata per questo esempio. Non sembra essere angolare con cui ho familiarità.
deepak

La lingua era javascript, la sintassi era coffeescript.
biofrattale

1
@jlguenego Hai una cronologia del browser funzionale / bowser pulsanti avanti e indietro e URL che puoi aggiungere ai segnalibri.
Torsten Barthel

3
@jlguenego - le risposte che suggeriscono di utilizzare variazioni di $window.history.back()hanno perso una parte cruciale della domanda. Il punto era ripristinare lo stato dell'applicazione nella posizione dello stato corretta quando la cronologia salta (indietro / avanti / aggiorna). Ciò sarebbe normalmente ottenuto fornendo i dati di stato tramite l'URI. La domanda chiedeva come passare da una posizione di stato all'altra senza dati di stato URI (espliciti). Dato questo vincolo, non è sufficiente replicare semplicemente il pulsante Indietro perché questo si basa sui dati di stato dell'URI per ristabilire la posizione dello stato.
biofrattale

46
app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) {
  $rootScope.goBack = function(){
    $window.history.back();
  }
}]);

<a href="#" ng-click="goBack()">Back</a>

2
ama questo! ... lol ... per la chiarezza non $window.history.backsta facendo la magia $rootScope... quindi tornare indietro potrebbe essere vincolato all'ambito della direttiva della barra di navigazione se lo desideri.
Benjamin Conant

@BenjaminConant Per le persone che non sanno come implementarlo, devi semplicemente inserire $window.history.back();in una funzione su cui essere chiamato ng-click.
chakeda

rootScope corretto serve solo a rendere la funzione accessibile in qualsiasi modello
Guillaume Massé

Cena di pollo.
Cody

23

Dopo aver testato diverse proposte, ho scoperto che il modo più semplice è spesso il migliore.

Se usi un ui-router angolare e hai bisogno di un pulsante per tornare indietro meglio è questo:

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

o

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

// Avviso non impostare href o il percorso verrà interrotto.

Spiegazione: supponiamo un'applicazione di gestione standard. Cerca oggetto -> Visualizza oggetto -> Modifica oggetto

Utilizzo delle soluzioni angolari Da questo stato:

Cerca -> Visualizza -> Modifica

Per :

Cerca -> Visualizza

Bene, questo è quello che volevamo, tranne se ora fai clic sul pulsante Indietro del browser sarai di nuovo lì:

Cerca -> Visualizza -> Modifica

E questo non è logico

Tuttavia utilizzando la soluzione semplice

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

a partire dal :

Cerca -> Visualizza -> Modifica

dopo aver cliccato sul pulsante:

Cerca -> Visualizza

dopo aver fatto clic sul pulsante Indietro del browser:

Ricerca

La coerenza è rispettata. :-)


7

Se stai cercando il più semplice pulsante "indietro", potresti impostare una direttiva in questo modo:

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

Tieni presente che questo è un pulsante "indietro" abbastanza poco intelligente perché utilizza la cronologia del browser. Se lo includi nella tua pagina di destinazione, restituirà un utente a qualsiasi URL da cui proveniva prima di atterrare sul tuo.


3

soluzione del pulsante Indietro / Avanti del browser
Ho riscontrato lo stesso problema e l'ho risolto utilizzando l' popstate eventoggetto $ window e ui-router's $state object. Un evento popstate viene inviato alla finestra ogni volta che la voce della cronologia attiva cambia.
Gli eventi $stateChangeSuccesse $locationChangeSuccessnon vengono attivati ​​al clic sul pulsante del browser anche se la barra degli indirizzi indica la nuova posizione.
Quindi, supponendo che hai navigato da stati mainal folderdi mainnuovo, quando si preme backsul browser, si dovrebbe essere di nuovo al folderpercorso. Il percorso viene aggiornato ma la vista non lo è e mostra ancora tutto ciò che hai main. prova questo:

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

I pulsanti indietro / avanti dovrebbero funzionare senza problemi dopo di che.

nota: controlla la compatibilità del browser per window.onpopstate () per essere sicuro


3

Può essere risolto utilizzando una semplice direttiva "go-back-history", anche questa chiude la finestra in caso di assenza di cronologia precedente.

Utilizzo della direttiva

<a data-go-back-history>Previous State</a>

Dichiarazione direttiva angolare

.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: lavorare utilizzando ui-router o meno.


0

Anche il pulsante Indietro non funzionava per me, ma ho capito che il problema era che avevo dei htmlcontenuti all'interno della mia pagina principale, ui-viewnell'elemento.

cioè

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

Quindi ho spostato il contenuto in un nuovo .htmlfile e l'ho contrassegnato come modello nel .jsfile con i percorsi.

cioè

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

-1

history.back()e passare allo stato precedente spesso danno l'effetto che non desideri. Ad esempio, se si dispone di un modulo con schede e ogni scheda ha il proprio stato, questa è appena stata selezionata la scheda precedente, non ritorna dal modulo. In caso di stati annidati, di solito è necessario quindi pensare a quali stati genitori si desidera ripristinare.

Questa direttiva risolve il problema

angular.module('app', ['ui-router-back'])

<span ui-back='defaultState'> Go back </span>

Ritorna allo stato che era attivo prima che il pulsante fosse visualizzato. Facoltativo defaultStateè il nome dello stato utilizzato quando nessuno stato precedente in memoria. Inoltre ripristina la posizione di scorrimento

Codice

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;
                });
        }]);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.