Come posso far funzionare il pulsante Indietro con una macchina a stati del router per router AngularJS?

Ho implementato un’applicazione angularjs a pagina singola usando ui-router .

Inizialmente identificavo ogni stato usando un url distinto, tuttavia ciò avveniva per gli url impaccati GUID.

Così ora ho definito il mio sito come una macchina a stati molto più semplice. Gli stati non sono identificati da url ma vengono semplicemente trasferiti come richiesto, come questo:

Definire gli 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 syntax pulita. Tuttavia, poiché non sto utilizzando gli URL, il pulsante Indietro non funziona.

Come posso mantenere la mia macchina da stato del router ui ma abilitare la funzionalità del pulsante Indietro?

Nota

Le risposte che suggeriscono di utilizzare le varianti di $window.history.back() hanno tutte perso una parte cruciale della domanda: come ripristinare lo stato dell’applicazione nella posizione di stato corretta mentre la cronologia salta (indietro / avanti / aggiornamento). Con quello in mente; per favore, continua a leggere


Sì, è ansible avere il browser indietro / avanti (cronologia) e aggiornare mentre si esegue una macchina dello stato del ui-router puro, ma ci vuole un po ‘di lavoro.

Hai bisogno di diversi componenti:

  • URL unici . Il browser triggers solo i pulsanti Indietro / Avanti quando cambi URL, quindi devi generare un URL unico 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 url-state in modo che sia ansible recuperare le informazioni di stato dopo che l’app angular è stata riavviata indietro / avanti o aggiornare i clic.

  • Una storia di stato . Un semplice dizionario di stati di ui-router digitato da un unico URL. Se puoi fare affidamento su HTML5, puoi utilizzare l’ API di cronologia HTML5 , ma se, come me, non puoi, puoi implementarlo tu stesso in poche righe di codice (vedi sotto).

  • Un servizio di localizzazione . Infine, devi essere in grado di gestire sia le modifiche dello stato del router, triggerste internamente dal tuo codice, sia le normali modifiche dell’URL del browser in genere triggerste dall’utente che fa clic sui pulsanti del browser o digita roba nella barra del browser. Tutto questo può diventare un po ‘complicato perché è facile confondersi su cosa ha scatenato 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’object sessionStorage javascript. L’ho tagliato per chiarezza qui. Per una spiegazione completa di questo si veda: Come gestisco l’aggiornamento della pagina con un’applicazione per pagina singola AngularJS

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 

StateHistoryService occupa della memorizzazione e del recupero degli stati storici mediante la StateHistoryService URL unici e generati. In realtà è solo un comodo wrapper per un object di stile del dizionario.

Il servizio di localizzazione dello stato

 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 

Lo StateLocationService gestisce due eventi:

  • locationChange . Questo 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 o quando l’utente digita un URL. Se nello StateHistoryService esiste uno stato per la posizione corrente.url, viene utilizzato per ripristinare lo stato tramite $state.go del router.

  • statoCambia . Questo viene chiamato quando si sposta lo stato internamente. Il nome e i parametri dello stato corrente sono memorizzati nello StateHistoryService digitato da un url generato. Questo URL generato può essere qualsiasi cosa tu voglia, potrebbe o meno identificare lo stato in un modo leggibile dall’uomo. Nel mio caso sto usando uno stato param più una sequenza generata a caso di cifre derivate da un guid (vedi il piede per lo snippet del generatore di guida). L’url generato viene visualizzato nella barra del browser e, in modo cruciale, aggiunto allo stack cronologico interno del browser utilizzando l’ @location.url url . Aggiunge l’url allo stack cronologico del browser che abilita i pulsanti avanti / indietro.

Il grosso problema con questa tecnica è che chiamare l’ @location.url url nel metodo stateChange attiverà l’evento $locationChangeSuccess e quindi chiamerà il metodo locationChange . Allo stesso modo chiamare @state.go da locationChange attiverà l’evento $stateChangeSuccess e quindi il metodo stateChange . Questo diventa molto confuso e rovina la storia del browser senza fine.

La soluzione è molto semplice. È ansible vedere l’array preventCall utilizzato come stack ( pop e push ). Ogni volta che viene chiamato uno dei metodi impedisce che l’altro metodo venga chiamato solo una volta. Questa tecnica non interferisce con il corretto innesco degli eventi $ e mantiene tutto dritto.

Ora tutto ciò che dobbiamo fare è chiamare i metodi di HistoryService al momento opportuno nel ciclo di vita della transizione di stato. Questo è fatto nel metodo .run AngularJS Apps, come questo:

App.run angular

 angular .module 'app', ['ui.router'] .run ($rootScope, stateLocationService) -> $rootScope.$on '$stateChangeSuccess', (event, toState, toParams) -> stateLocationService.stateChange() $rootScope.$on '$locationChangeSuccess', -> stateLocationService.locationChange() 

Generare 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 questo a posto, i pulsanti avanti / indietro e il pulsante di aggiornamento funzionano tutti come previsto.

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

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

Se utilizzi un ui-router angular e hai bisogno di un pulsante per tornare indietro, questo è il seguente:

  

o

  Back  

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. 🙂

Se stai cercando il pulsante “indietro” più semplice, puoi 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é sta utilizzando la cronologia del browser. Se lo includi nella tua pagina di destinazione, invierà un utente a qualsiasi URL proveniente prima dell’atterraggio sul tuo.

soluzione per i pulsanti back / forward del browser
Ho riscontrato lo stesso problema e l’ho risolto utilizzando l’ popstate event dall’object $ window e dall’object ui-router's $state object . Un evento popstate viene inviato alla finestra ogni volta che cambia la voce della cronologia triggers.
Gli $stateChangeSuccess e $locationChangeSuccess non vengono triggersti ​​sul clic del pulsante del browser anche se la barra degli indirizzi indica la nuova posizione.
Quindi, supponendo che tu abbia navigato di nuovo tra stati dalla folder main alla folder main , quando back sul browser, dovresti tornare al percorso della folder . Il percorso è aggiornato ma la vista non lo è e visualizza comunque tutto ciò che si ha sul 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 = {}; // ie- $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.

nota: verificare la compatibilità del browser per window.onpopstate () per sicurezza

Può essere risolto usando una semplice direttiva “cronologia di ritorno”, questa è anche finestra di chiusura in caso di assenza di cronologia precedente.

Uso della direttiva

 Previous State 

Dichiarazione direttiva 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: utilizzo di ui-router o meno.

Anche il pulsante Indietro non funzionava per me, ma ho capito che il problema era che avevo contenuto html all’interno della mia pagina principale, nell’elemento ui-view .

vale a dire

 

Hey Kids!

Così ho spostato il contenuto in un nuovo file .html e l’ho contrassegnato come modello nel file .js con i percorsi.

vale a dire

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

history.back() e passa allo stato precedente spesso danno l’effetto non desiderato. Ad esempio, se hai un modulo con le tabs e ogni scheda ha il proprio stato, questo ha appena cambiato la scheda precedente selezionata, non restituita dal modulo. In caso di stati annidati, di solito è necessario pensare alla stregua degli stati genitore che si desidera ripristinare.

Questa direttiva risolve il problema

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

Ritorna allo stato, che era attivo prima che il pulsante fosse visualizzato. Opzionale defaultState è il nome dello stato utilizzato quando non ci sono stati precedenti 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((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; }); }]);