web-dev-qa-db-fra.com

Comment faire en sorte que le bouton Précédent fonctionne avec une machine d'état AngularJS ui-router?

J'ai implémenté une application simple page angularjs utilisant ui-router

A l'origine, j'avais identifié chaque état à l'aide d'une URL distincte, mais cela créait des URL GUID inamicales et peu conviviales.

J'ai donc défini mon site comme une machine à états beaucoup plus simple. Les états ne sont pas identifiés par des URL, mais sont simplement transférés comme requis, comme ceci:

Définir les états imbriqués

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'adore sa syntaxe épurée. Cependant, comme je n’utilise pas d’URL, le bouton Retour ne fonctionne pas. 

Comment garder ma machine d'état ordonnée du routeur ui tout en activant la fonctionnalité du bouton Précédent? 

83
biofractal

Remarque

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'emplacement correct lorsque l'historique saute (retour/retour/rafraîchissement). Dans cet esprit; s'il vous plaît, lisez la suite.


Oui, il est possible d'avoir le navigateur précédent/suivant (actualisé) et d'actualiser tout en exécutant une machine d'état ui-router pure, mais cela prend un peu de temps. 

Vous avez besoin de plusieurs composants:

  • URL uniques. Le navigateur active uniquement les boutons Précédent/Suivant lorsque vous modifiez des URL. Vous devez donc générer une URL unique par état visité. Ces URL ne doivent cependant contenir aucune information d'état.

  • Un service de session. Chaque URL générée est corrélée à un état particulier. Vous avez donc besoin d’un moyen de stocker vos paires d’URL-état afin de pouvoir récupérer les informations sur l’état après le redémarrage de votre application angulaire par des clics précédent/suivant ou à rafraîchir.

  • Une histoire d'état. Un dictionnaire simple des états ui-router, clé par url unique. Si vous pouvez vous fier à HTML5, vous pouvez utiliser HTML5 History API , mais si, comme moi, vous ne le pouvez pas, vous pouvez l'implémenter vous-même dans quelques lignes de code (voir ci-dessous).

  • Un service de localisation. Enfin, vous devez être en mesure de gérer les modifications de l'état du routeur ui, déclenchées en interne par votre code, et les modifications normales de l'URL du navigateur, généralement déclenchées par l'utilisateur qui clique sur les boutons du navigateur ou tape des éléments dans la barre de navigation. Tout cela peut devenir un peu délicat, car il est facile de ne pas savoir ce qui a déclenché quoi.

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 un wrapper pour l'objet javascript sessionStorage. Je l'ai coupé pour plus de clarté ici. Pour une explication complète de cette situation, veuillez consulter: Comment gérer l'actualisation d'une page avec une application simple page AngularJS

Le service d'histoire de l'État

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

La StateHistoryService s'occupe du stockage et de la récupération des états historiques associés aux URL uniques générées. Il s’agit en réalité d’un simple wrapper de commodité pour un objet de style dictionnaire.

Le service de localisation géographique

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 StateLocationService gère deux événements:

  • locationChange. Ceci est appelé lorsque l'emplacement des navigateurs est modifié, généralement lorsque le bouton Précédent/Suivant/actualiser est activé, lorsque l'application démarre pour la première fois ou lorsque l'utilisateur tape une URL. Si un état pour l'emplacement actuel.url existe dans la variable StateHistoryService, il est utilisé pour restaurer l'état via le $state.go de ui-router.

  • stateChange. Ceci est appelé lorsque vous déplacez l’état en interne. Le nom et les paramètres de l'état actuel sont stockés dans la StateHistoryService associée à une URL générée. Cette URL générée peut être ce que vous voulez, elle peut ou non identifier l'état de manière lisible par l'homme. Dans mon cas, j'utilise un état param et une séquence de chiffres générés de manière aléatoire, dérivés d'un guid (voir le pied pour l'extrait de générateur de guid). L'URL générée est affichée dans la barre de navigateur et, de manière cruciale, ajoutée à la pile d'historique interne du navigateur à l'aide de @location.url url. Il ajoute l’URL à la pile d’historique du navigateur qui active les boutons Précédent/Suivant.

Le gros problème de cette technique est que l'appel de @location.url url dans la méthode stateChange déclenchera l'événement $locationChangeSuccess et appellera la méthode locationChange. De même, l'appel de @state.go à partir de locationChange déclenchera l'événement $stateChangeSuccess et donc la méthode stateChange. Cela devient très déroutant et perturbe l’historique du navigateur.

La solution est très simple. Vous pouvez voir que le tableau preventCall est utilisé en tant que pile (pop et Push). Chaque fois que l'une des méthodes est appelée, elle empêche l'autre méthode d'être appelée une seule fois. Cette technique n'interfère pas avec le déclenchement correct des événements $ et maintient tout droit.

Il ne nous reste plus qu'à appeler les méthodes HistoryService au moment opportun dans le cycle de vie de la transition d'état. Ceci est fait dans la méthode AngularJS Apps .run, comme ceci:

App angulaire.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 Précédent/Suivant et le bouton Actualiser fonctionnent comme prévu.

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

<a href="#" ng-click="goBack()">Back</a>
46
Guillaume Massé

Après avoir testé différentes propositions, j’ai trouvé que le moyen le plus simple est souvent le meilleur.

Si vous utilisez un routeur ui angulaire et que vous avez besoin d’un bouton pour revenir en arrière, voici ce qui se passe:

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

ou

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

// Attention, ne définissez pas le href ou le chemin sera cassé.

Explication: Supposons une application de gestion standard Objet de recherche -> Objet de vue -> Editer un objet

Utilisation des solutions angulaires À partir de cet état:

Rechercher -> Vue -> Modifier

À :

Rechercher -> Voir

Eh bien, c’est ce que nous voulions, sauf que si vous cliquez maintenant sur le bouton Précédent du navigateur, vous serez de nouveau présent:

Rechercher -> Vue -> Modifier

Et ce n'est pas logique

Cependant en utilisant la solution simple

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

de :

Rechercher -> Vue -> Modifier

après avoir cliqué sur le bouton:

Rechercher -> Voir

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

Chercher

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

22
bArraxas

Si vous recherchez le bouton "retour" le plus simple, 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 peu intelligent, car il utilise l'historique du navigateur. Si vous l'incluez sur votre page de destination, un utilisateur sera renvoyé à son URL d'origine avant de se retrouver sur la vôtre.

7
dgrant069

la solution du bouton Précédent/Suivant du navigateur
J'ai rencontré le même problème et je l'ai résolu en utilisant le popstate event de l'objet $ window et le ui-router's $state object. Un événement popstate est distribué dans la fenêtre chaque fois que l'entrée d'historique active est modifiée.
Les événements $stateChangeSuccess et $locationChangeSuccess ne sont pas déclenchés par un clic sur le bouton du navigateur, même si la barre d'adresse indique le nouvel emplacement.
Donc, en supposant que vous ayez à nouveau navigué des états main à folder à main, lorsque vous cliquez sur back sur le navigateur, vous devriez revenir à la route folder. Le chemin est mis à jour mais la vue ne s'affiche pas et affiche toujours ce que vous avez sur main. essaye ça:

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 Précédent/Suivant devraient fonctionner normalement après cela.

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

3
jfab fab

Peut être résolu en utilisant une simple directive "historique de l'historique", celle-ci ferme également la fenêtre en l'absence d'historique.

Utilisation de la directive

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

Déclaration de 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();  
                }
            });
        }
    };
}])

Remarque: Travailler avec ui-router ou non.

3
hthetiot

Le bouton Précédent ne fonctionnait pas non plus pour moi, mais j'ai découvert que le problème était que le contenu de mon nom html se trouvait dans ma page principale, dans l'élément ui-view.

c'est à dire. 

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

J'ai donc déplacé le contenu dans un nouveau fichier .html et l'ai marqué en tant que modèle dans le fichier .js avec les itinéraires. 

c'est à dire.

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