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?
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.
app.run(['$window', '$rootScope',
function ($window , $rootScope) {
$rootScope.goBack = function(){
$window.history.back();
}
}]);
<a href="#" ng-click="goBack()">Back</a>
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. :-)
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.
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
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.
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"
})