Como faço para que o botão Voltar funcione com uma máquina de estado do roteador ui AngularJS?

87

Implementei um aplicativo de página única angularjs usando ui-router .

Originalmente, identifiquei cada estado usando um url distinto, mas isso tornava urls compactadas com GUID hostis.

Portanto, agora defini meu site como uma máquina de estados muito mais simples. Os estados não são identificados por urls, mas são simplesmente transferidos conforme necessário, como este:

Definir estados aninhados

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 sua sintaxe limpa. No entanto, como não estou usando urls, o botão Voltar não funciona.

Como mantenho minha máquina de estado ui-router organizada, mas habilito a funcionalidade do botão Voltar?

biofractal
fonte
1
"estados não são identificados por urls" - e esse é o seu problema, eu suspeito. O botão Voltar é bastante protegido do código (caso contrário, as pessoas o substituiriam, causando problemas). Por que não deixar o angular criar urls melhores, como o SO faz (OK, eles podem não estar usando angular, mas o esquema de URL é ilustrativo)?
jcollum
Além disso, esta pergunta pode ajudar: stackoverflow.com/questions/13499040/…
jcollum
Além disso, como você não está usando URLs, isso não significa que, para chegar ao estado Z, as pessoas terão que clicar nos estados X e Y para acessá-lo? Isso pode ser irritante.
jcollum
irá com o estado com parâmetros diferentes? @jcollum
VijayVishnu
Não tenho ideia, isso foi há muito tempo
jcollum

Respostas:

78

Nota

Todas 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 conforme o histórico pula (voltar / avançar / atualizar). Com aquilo em mente; por favor, continue a ler.


Sim, é possível retroceder / avançar (histórico) e atualizar o navegador durante a execução de uma ui-routermáquina de estado puro , mas isso demora um pouco.

Você precisa de vários componentes:

  • URLs exclusivos . O navegador só ativa os botões voltar / avançar quando você altera os urls, portanto, você deve gerar um url exclusivo para cada estado visitado. No entanto, esses urls não precisam conter nenhuma informação de estado.

  • Um serviço de sessão . Cada url gerado é correlacionado a um estado específico, portanto, você precisa de uma maneira de armazenar seus pares de url-estado para que possa recuperar as informações de estado depois que seu aplicativo angular foi reiniciado por retroceder / avançar ou cliques de atualização.

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

  • Um serviço de localização . Finalmente, você precisa ser capaz de gerenciar as alterações de estado do roteador da interface do usuário, acionadas internamente por 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 ser um pouco complicado porque é fácil ficar confuso sobre o que acionou o quê.

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

O Serviço de Sessão

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 sessionStorageobjeto javascript . Eu o cortei para maior clareza aqui. Para obter uma explicação completa sobre isso, consulte: Como faço para atualizar a página 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 StateHistoryServicecuidado com o armazenamento e recuperação de estados históricos digitados por urls únicas geradas. Na verdade, é apenas um invólucro prático para um objeto de estilo de dicionário.

O Serviço de Localização Estadual

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 StateLocationServicetrata de dois eventos:

  • locationChange . Isso é chamado quando a localização do navegador é alterada, normalmente quando o botão voltar / avançar / atualizar é pressionado ou quando o aplicativo é iniciado pela primeira vez ou quando o usuário digita um url. Se um estado para o location.url atual existir no StateHistoryService, ele será usado para restaurar o estado por meio do ui-roteador $state.go.

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

O grande problema com essa técnica é que chamar @location.url urlo stateChangemétodo irá disparar o $locationChangeSuccessevento e então chamar o locationChangemétodo. Igualmente, chamar o @state.gofrom locationChangeirá disparar o $stateChangeSuccessevento e, portanto, o stateChangemétodo. Isso fica muito confuso e bagunça o histórico do navegador sem fim.

A solução é muito simples. Você pode ver a preventCallmatriz sendo usada como uma pilha ( pope push). Cada vez que um dos métodos é chamado, ele evita que o outro método seja chamado apenas uma vez. Essa técnica não interfere no disparo correto dos $ events e mantém tudo correto.

Agora tudo o que precisamos fazer é chamar os HistoryServicemétodos no momento apropriado no ciclo de vida de transição de estado. Isso é feito no .runmétodo AngularJS Apps , como este:

Angular app.run

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

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

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

Gerar 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 avançar / voltar e o botão Atualizar funcionam como esperado.

biofractal
fonte
1
no exemplo do SessionService acima, acho que o método accessor: deve estar usando @setStoragee em @getStoragevez de `@ get / setSession '.
Peter Whitfield
3
Qual idioma é usado para este exemplo. Não parece ser angular que eu conheço.
deepak
A linguagem era javascript, a sintaxe era coffeescript.
biofractal de
1
@jlguenego Você tem um histórico funcional do navegador / botões de navegação para frente e para trás e URLs que você pode marcar.
Torsten Barthel de
3
@jlguenego - as respostas que sugerem o uso de variações de $window.history.back()perderam uma parte crucial da pergunta. O objetivo era restaurar o estado do aplicativo para o local de estado correto conforme o histórico pula (voltar / avançar / atualizar). Isso normalmente seria obtido fornecendo os dados de estado por meio do URI. A questão perguntada como pular entre locais de estado sem dados de estado de URI (explícitos). Dada essa restrição, não é suficiente apenas replicar o botão Voltar, porque isso depende dos dados de estado do URI para restabelecer a localização do 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é
fonte
2
amo isso! ... lol ... para a clareza não $window.history.backestá fazendo a mágica $rootScope... então volte pode ser vinculado ao escopo da sua diretiva navbar se quiser.
Benjamin Conant
@BenjaminConant Para pessoas que não sabem como implementar isso, você simplesmente coloca o $window.history.back();em uma função a ser chamada ng-click.
chakeda
rootScope correto serve apenas para tornar a função acessível em qualquer template
Guillaume Massé
Jantar de Frango.
Cody
23

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

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

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

ou

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

// Aviso, não defina o href ou o caminho será quebrado.

Explicação: Suponha um aplicativo de gerenciamento padrão. Objeto de pesquisa -> Exibir objeto -> Editar objeto

Usando as soluções angulares a partir deste estado:

Pesquisar -> Exibir -> Editar

Para :

Pesquisar -> Exibir

Bem, isso é o que queríamos, exceto se agora você clicar no botão Voltar do navegador, você estará lá novamente:

Pesquisar -> Exibir -> Editar

E isso não é lógico

No entanto, usando a solução simples

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

de :

Pesquisar -> Exibir -> Editar

depois de clicar no botão:

Pesquisar -> Exibir

depois de clicar no botão Voltar do navegador:

Procurar

A consistência é respeitada. :-)

bArraxas
fonte
7

Se você está procurando o botão "voltar" mais simples, 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();
          };
        }
      };
    });

Lembre-se de que este é um botão "voltar" bastante pouco inteligente porque está usando o histórico do navegador. Se você incluí-lo em sua página de destino, ele enviará o usuário de volta a qualquer url de onde veio antes de acessar o seu.

dgrant069
fonte
3

solução do botão voltar / avançar do navegador
Encontrei o mesmo problema e resolvi-o usando o popstate eventobjeto da janela $ e ui-router's $state object. Um evento popstate é despachado para a janela sempre que a entrada ativa do histórico é alterada.
Os eventos $stateChangeSuccesse $locationChangeSuccessnão são acionados no clique do botão do navegador, embora a barra de endereço indique o novo local.
Assim, supondo que você tenha navegado dos estados mainpara foldera mainoutra vez, quando você bate backno navegador, você deve estar de volta à folderrota. O caminho é atualizado, mas a visualização não é e ainda exibe tudo o que você tem 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 = {};  // 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}
            );
        }
    };
}

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

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

jfab fab
fonte
3

Pode ser resolvido usando uma simples diretiva "go-back-history", esta também fecha a janela caso não haja histórico anterior.

Uso de diretiva

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

Declaração de diretiva 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: Trabalhar usando ui-router ou não.

Hthetiot
fonte
0

O botão Voltar não estava funcionando bem para mim, mas descobri que o problema era que eu tinha htmlconteúdo dentro da minha página principal, no ui-viewelemento.

ie

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

Então, movi o conteúdo para um novo .htmlarquivo e marquei-o como um modelo no .jsarquivo com as rotas.

ie

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

history.back()e alternar para o estado anterior geralmente dá o efeito que você deseja. Por exemplo, se você tiver um formulário com guias e cada guia tiver seu próprio estado, isso apenas alternará a guia anterior selecionada, não retornará do formulário. No caso de estados aninhados, você geralmente precisa pensar sobre quais estados pai deseja reverter.

Esta diretiva resolve o problema

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

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

Ele retorna ao estado que estava ativo antes de o botão ser exibido. Opcional defaultStateé o nome do estado usado quando não há estado anterior na memória. 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((<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
fonte