Comment puis-je obtenir le bouton arrière pour travailler avec une machine D'état de routeur-ui AngularJS?

j'ai implémenté une application de page unique angularjs en utilisant ui-router .

à l'origine, j'ai identifié chaque État à l'aide d'une url distincte, mais cela a fait pour les urls Non conviviales et emballées.

donc j'ai maintenant défini mon site comme une machine d'état beaucoup plus simple. Les États ne sont pas identifiés par des urls mais sont simplement transitionnés en tant que de besoin, comme ceci:

Definit Nested States

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'

Transition vers un État défini

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

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

Ce système fonctionne très bien et j'aime sa propre syntaxe. Cependant, comme je n'utilise pas d'URL, le bouton back ne fonctionne pas.

Comment puis-je garder mon ui-router state-machine mais activer la fonctionnalité du bouton arrière?

83
demandé sur mark 2014-03-07 14:16:44

8 réponses

Note

les réponses qui suggèrent d'utiliser des variantes de $window.history.back() ont toutes manqué une partie cruciale de la question: Comment restaurer l'état de l'application à l'État-emplacement correct pendant que l'histoire saute (retour/avant/rafraîchir). Avec cela à l'esprit, s'il vous plaît, lisez la suite.


Oui, il est possible d'avoir le navigateur en arrière/avant (histoire) et rafraîchir tout en exécutant une pure ui-router state-machine mais il faut un peu de faire.

vous avez besoin de plusieurs composants:

  • URLs Uniques . Le navigateur n'active les boutons back/forward que lorsque vous changez d'URL, vous devez donc générer une url unique par état visité. Ces urls n'ont pas besoin de contenir des informations d'état.

  • Une Session De Service . Chaque url générée est corrélée à un état particulier de sorte que vous avez besoin d'un moyen de stocker vos paires d'États url-de sorte que vous pouvez récupérer les informations de l'état après votre application angulaire a été redémarré par des clics de retour / avant ou de rafraîchir.

  • Un État De L'Histoire . Un dictionnaire simple des États ui-router tapés par url unique. Si vous pouvez compter sur HTML5, vous pouvez utiliser L'API HTML5 History , mais si, comme moi, vous ne pouvez pas alors vous pouvez l'implémenter vous-même dans quelques lignes de code (voir ci-dessous).

  • "1519300920 Un" Service De Localisation . Enfin, vous devez être capable de gérer à la fois les changements d'état du routeur-ui, déclenchés en interne par votre code, et les changements d'url normaux du navigateur typiquement déclenchés par l'utilisateur en cliquant sur les boutons du navigateur ou en tapant des trucs dans la barre de navigateur. Tout cela peut devenir un peu délicat parce qu'il est facile de se confondre sur ce que déclenché.

Voici ma mise en œuvre de chacune de ces exigences. J'ai tout regroupé en trois services:

Le Service 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

c'est une enveloppe pour l'objet javascript sessionStorage . J'ai coupé vers le bas pour plus de clarté ici. Pour une explication complète de ce s'il vous plaît voir: Comment puis-je gérer page rafraîchissante avec un AngularJS Single Page Application

L'État De L'Histoire De Service

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

le StateHistoryService s'occupe du stockage et de la récupération des États historiques saisis par des urls uniques. Il est vraiment juste un emballage de commodité pour un objet de style de dictionnaire.

L'État De Service De Localisation

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

le StateLocationService gère deux événements:

  • locationChange . Cela s'appelle lorsque l'emplacement du navigateur est modifié, généralement lorsque le bouton "back/forward/refresh" est appuyé ou lorsque l'application démarre pour la première fois ou lorsque l'utilisateur tape une url. Si un état de l'emplacement actuel.url existe dans le StateHistoryService puis il est utilisé pour restaurer l'état via ui-router $state.go .

  • stateChange . Cela s'appelle lorsque vous déplacez l'état en interne. Le nom et les paramètres de l'état courant sont stockés dans le StateHistoryService saisi par une url générée. Cette url générée peut être tout ce que vous voulez, elle peut ou non identifier l'état d'une manière lisible par l'humain. Dans mon cas, j'utilise un État param plus une séquence de chiffres générée au hasard dérivée d'une guid (voir pied pour le générateur de guid snippet). L'url générée est affichée dans la barre de navigation et, surtout, ajouté à la pile d'historique interne du navigateur en utilisant @location.url url . Son ajout de l'url à la pile historique du navigateur qui permet les boutons forward / back.

le gros problème avec cette technique est qu'appeler @location.url url dans la méthode stateChange déclenchera l'événement $locationChangeSuccess et donc appeler la méthode locationChange . Le fait d'appeler @state.go de locationChange déclenchera l'événement $stateChangeSuccess et donc la méthode stateChange . Cela devient très confus et gâche l'historique du navigateur sans fin.

la solution est très simple. Vous pouvez voir le tableau preventCall utilisé comme une pile ( pop et push ). Chaque fois que l'une des méthodes est appelée, elle empêche l'autre méthode appelée qu'une seule fois. Cette technique n'interfère pas avec le déclenchement correct des $ events et garde tout droit.

maintenant tout nous devons faire appel aux méthodes HistoryService au moment approprié du cycle de vie de la transition d'état. Ceci est fait dans L'application AngularJS .run méthode, comme ceci:

Angulaire de l'app.run

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

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

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

Générer un Guid

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

avec tout cela en place, les boutons avant / arrière et le bouton de rafraîchissement fonctionnent comme prévu.

72
répondu biofractal 2017-12-12 09:46:58
app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) {
  $rootScope.goBack = function(){
    $window.history.back();
  }
}]);

<a href="#" ng-click="goBack()">Back</a>
46
répondu Guillaume Massé 2014-12-08 00:17:43

après avoir testé différentes propositions, j'ai constaté que la manière la plus facile est souvent la meilleure.

si vous utilisez U-routeur angulaire et que vous avez besoin d'un bouton pour revenir en arrière est le suivant:

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

ou

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

// Avertissement ne réglez pas le href, ou le chemin d'accès sera brisé.

explication: Supposons une application de gestion standard. Objet de recherche - > objet de vue - > objet D'édition

utilisant les solutions angulaires De cet état:

Recherche - > View - > Edit

à:

Recherche - > Voir

Eh bien c'est ce que nous voulions sauf si maintenant vous cliquez sur le bouton Retour du navigateur vous serez là à nouveau:

Recherche - > View - > Edit

et ce n'est pas logique

Cependant, en utilisant la solution simple

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

de:

Recherche - > View - > Edit

après avoir cliqué sur le bouton:

Recherche - > Voir

après avoir cliqué sur le bouton Retour du navigateur:

recherche

la cohérence est respectée. :- )

22
répondu bArraxas 2017-02-08 08:47:44

si vous recherchez le plus simple bouton "back", alors vous pouvez configurer une directive comme ceci:

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

gardez à l'esprit qu'il s'agit d'un bouton" Retour " assez inintelligent, car il utilise l'historique du navigateur. Si vous l'incluez sur votre page d'accueil, il enverra un utilisateur de retour à n'importe quelle url qu'ils sont venus avant d'atterrir sur la vôtre.

7
répondu dgrant069 2014-07-16 20:27:49

du navigateur en arrière/avant du bouton solution

J'ai rencontré le même problème et je l'ai résolu en utilisant le popstate event de l'objet $window et ui-router's $state object . Un événement popstate est envoyé à la fenêtre à chaque fois que l'entrée Active history change.

Les événements $stateChangeSuccess et $locationChangeSuccess ne sont pas déclenchés par le clic du bouton du navigateur même si la barre d'adresse indique le nouvel emplacement.

Donc, en supposant que vous avez navigué des États main à folder à main encore une fois, lorsque vous appuyez sur back sur le navigateur, vous devriez revenir à la route folder . Le chemin est à jour, mais la vue n'est pas et affiche toujours ce que vous avez sur main . essayez ceci:

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

marche arrière/avant devraient fonctionner en douceur après cela.

note: vérifiez la compatibilité du navigateur pour window.onpopstate() sûr

3
répondu jfab fab 2017-02-17 20:47:19

peut être résolu en utilisant une simple directive" go-back-history", celle-ci ferme également la fenêtre en cas d'absence d'histoire antérieure.

l'utilisation de la Directive

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

déclaration directive angulaire

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

Note: Utilisation du routeur ui ou non.

3
répondu hthetiot 2017-04-01 22:44:29

le bouton arrière ne fonctionnait pas pour moi aussi, mais j'ai compris que le problème était que j'avais html contenu dans ma page principale, dans l'élément ui-view .

c'est à dire

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

donc j'ai déplacé le contenu dans un nouveau fichier .html , et je l'ai marqué comme un modèle dans le fichier .js avec les routes.

c'est à dire

   .state("parent.mystuff", {
        url: "/mystuff",
        controller: 'myStuffCtrl',
        templateUrl: "myStuff.html"
    })
0
répondu CodyBugstein 2015-05-15 20:50:00

history.back() et passer à état précédent donnent souvent effet pas que vous voulez. Par exemple, si vous avez le formulaire avec des onglets et chaque onglet a son propre état, cela vient de changer l'onglet précédent sélectionné, ne pas retourner du formulaire. Dans les États emboîtés, vous avez généralement besoin de penser à la Sorcière des États parents que vous voulez faire reculer.

la Présente directive résout le problème

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

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

Il revient à l'état, qui était actif Le bouton précédant le bouton s'est affiché. Optionnel defaultState est le nom de l'état qui a utilisé quand aucun état précédent dans la mémoire. Il restaure également la position de défilement

Code

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;
                });
        }]);
-1
répondu Sel 2016-12-23 08:22:22