Como obtenho o botão Voltar para trabalhar com uma máquina de estado de roteador ui AngularJS?

Eu implementei um aplicativo de página única angularjs usando o roteador-ui .

Originalmente, identifiquei cada estado usando um URL distinto, mas isso foi feito para URLs não amigáveis ​​e repletas de GUID.

Então, agora defini meu site como uma máquina de estado muito mais simples. Os estados não são identificados por URLs, mas são simplesmente transferidos para conforme necessário, assim:

Definir estados nesteds

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' 

Transição para um estado definido

 #The ui-sref attrib transitions to the 'folder' state a(ui-sref="folder({locationId:'{{folder.Id}}'})") | {{ folder.Name }} 

Este sistema funciona muito bem e adoro a sua syntax limpa. No entanto, como não estou usando URLs, o botão Voltar não funciona.

Como eu mantenho minha máquina de estado de roteador ui puro, mas habilito a funcionalidade do botão de retorno?

Nota

As respostas que sugerem o uso de variações de $window.history.back() perderam uma parte crucial da questão: Como restaurar o estado do aplicativo para o local de estado correto à medida que o histórico salta (back / forward / refresh). Com aquilo em mente; por favor, continue a ler.


Sim, é possível ter o navegador de volta / avançar (histórico) e atualizar durante a execução de uma máquina de estado do ui-router puro, mas é preciso fazer um pouco.

Você precisa de vários componentes:

  • URLs únicos . O navegador só habilita os botões voltar / avançar quando você altera urls, portanto, você deve gerar um URL exclusivo por estado visitado. Esses URLs não precisam conter nenhuma informação de estado.

  • Um serviço de session . Cada URL gerada é correlacionada a um estado específico, portanto, você precisa de uma maneira de armazenar seus pares de estado de URL para poder recuperar as informações de estado depois que seu aplicativo angular for reiniciado com back / forward ou refresh cliques.

  • Uma história do estado . Um dictionary simples de estados do ui-roteador codificados por url exclusivo. Se você pode confiar no HTML5, então você pode usar a API History do HTML5 , mas se, como eu, você não puder, então você pode implementá-lo em algumas linhas de código (veja abaixo).

  • Um serviço de localização . Por fim, você precisa gerenciar as alterações de estado do ui-roteador, acionadas internamente pelo seu código e as alterações normais de URL do navegador normalmente acionadas pelo usuário clicando nos botões do navegador ou digitando coisas na barra do navegador. Isso tudo pode ficar um pouco complicado, porque é fácil ficar confuso sobre o que desencadeou o quê.

Aqui está a minha implementação de cada um desses requisitos. Eu juntei tudo em três serviços:

O serviço de session

 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 é um wrapper para o object javascript sessionStorage . Eu reduzi isto para clareza aqui. Para obter uma explicação completa sobre isso, consulte: Como lidar com a atualização de páginas com um aplicativo de página única AngularJS

O Serviço de História do 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 

O StateHistoryService cuida do armazenamento e da recuperação de estados históricos codificados por URLs gerados e exclusivos. É realmente apenas um invólucro de conveniência para um object de estilo de dictionary.

O serviço de localização do 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 

O StateLocationService lida com dois events:

  • locationChange . Isso é chamado quando o local dos navegadores é alterado, geralmente quando o botão voltar / avançar / atualizar é pressionado ou quando o aplicativo é iniciado pela primeira vez ou quando o usuário digita em um URL. Se houver um estado para o location.url atual no StateHistoryService , ele será usado para restaurar o estado via $state.go ui-router.

  • stateChange . Isso é chamado quando você move o estado internamente. O nome e os parâmetros do estado atual são armazenados no StateHistoryService codificado por um URL gerado. Este URL gerado pode ser o que você quiser, ele pode ou não identificar o estado de maneira legível. No meu caso, estou usando um parâmetro de estado mais uma seqüência de dígitos gerada aleatoriamente, derivada de um guid (veja o pé para o trecho do gerador de guia). O URL gerado é exibido na barra do navegador e, crucialmente, adicionado à pilha de histórico interno do navegador usando o @location.url url . É adicionar o URL à pilha de histórico do navegador que ativa os botões avançar / retroceder.

O grande problema com essa técnica é que chamar o @location.url url no método stateChange acionará o evento $locationChangeSuccess e, portanto, chamará o método locationChange . Igualmente, chamar o @state.go de locationChange acionará o evento $stateChangeSuccess e, portanto, o método stateChange . Isso fica muito confuso e atrapalha o histórico do navegador.

A solução é muito simples. Você pode ver o array preventCall sendo usado como uma pilha ( pop e push ). Cada vez que um dos methods é chamado, ele impede que o outro método seja chamado apenas uma vez. Essa técnica não interfere no acionamento correto dos $ events e mantém tudo correto.

Agora, tudo o que precisamos fazer é chamar os methods HistoryService no momento apropriado no ciclo de vida da transição de estado. Isso é feito no método .run do AngularJS Apps, assim:

App.run angular

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

Gere um Guid

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

Com tudo isso no lugar, os botões de avançar / voltar e o botão de atualização funcionam como esperado.

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

Depois de testar propostas diferentes, descobri que a maneira mais fácil costuma ser a melhor.

Se você usa um roteador ui angular e precisa de um botão para voltar melhor, é isso:

  

ou

  Back  

a partir de :

Pesquisar -> Visualizar -> Editar

depois clique no botão:

Pesquisa -> Visualizar

depois clique no botão Voltar do navegador:

Pesquisa

Consistência é respeitada. 🙂

Se você está procurando o botão “voltar” mais simples, então você pode configurar uma diretiva 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(); }; } }; }); 

Tenha em mente que este é um botão “back” bastante pouco inteligente porque está usando o histórico do navegador. Se você incluí-lo em sua página de destino, ele enviará um usuário de volta para qualquer URL da qual ele tenha vindo antes de aterrissar no seu.

Solução de botão de voltar / avançar do navegador
Eu encontrei o mesmo problema e resolvi-o usando o popstate event do object $ window e ui-router's $state object . Um evento popstate é enviado para a janela sempre que a input do histórico ativo é alterada.
Os $stateChangeSuccess e $locationChangeSuccess não são acionados no clique do botão do navegador, mesmo que a barra de endereço indique o novo local.
Portanto, supondo que você tenha navegado dos estados main para folder to main novamente, ao back no navegador, você deve voltar para a rota da folder . O caminho é atualizado, mas a exibição não é e ainda exibe o que você tem no main . tente isto:

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

botões de voltar / avançar devem funcionar sem problemas depois disso.

nota: verifique a compatibilidade do navegador para window.onpopstate () para ter certeza

Pode ser resolvido usando uma simples diretiva “go-back-history”, esta também está fechando a janela no caso de não haver histórico anterior.

Uso da diretiva

 Previous State 

Declaração da diretriz 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: Trabalhando usando o roteador ui ou não.

O botão Voltar não estava funcionando para mim também, mas eu percebi que o problema era que eu tinha conteúdo html dentro da minha página principal, no elemento ui-view .

ou seja

 

Hey Kids!

Então, movi o conteúdo para um novo arquivo .html e o marquei como um modelo no arquivo .js com as rotas.

ou seja

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

history.back() e mudar para o estado anterior, muitas vezes dar efeito que você não quer. Por exemplo, se você tiver um formulário com guias e cada guia tiver um estado próprio, isso apenas alterará a guia anterior selecionada e não retornará do formulário. No caso de estados nesteds, você geralmente precisa pensar sobre a bruxa dos estados paternais que você deseja reverter.

Esta diretiva resolve o problema

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

Ele retorna ao estado, que estava ativo antes do botão ser exibido. defaultState opcional é o nome do estado usado quando não há estado anterior na memory. Também restaura a posição de rolagem

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