Exemple d'application: http://angular.github.com/angular-phonecat/step-11/app/#/phones
Si vous choisissez le dernier téléphone "Motorola charm", il vous montrera les détails du téléphone. Lorsque vous revenez dans votre navigateur, il recharge les données et le défilement est en haut.
Quel est le meilleur moyen de faire automatiquement défiler jusqu’à l’endroit où il en restait lors de la navigation retour? .__ Et aussi, pourquoi angular recharge-t-il les données?
J'ai le même échantillon "angular-phonecat" sur mon ordinateur et j'ai ajouté un défilement infini qui charge plus de données à mesure que vous faites défiler. Donc, je ne veux vraiment pas que l'utilisateur recharge à nouveau plus de 50 articles ou fasse défiler l'écran pendant 30 secondes.
Je ne l'ai pas utilisé auparavant, mais angular a un service $ anchorScroll . En ce qui concerne le rechargement des données, vous pouvez le mettre en cache avec $ cacheFactory ou stocker les données sur une portée supérieure.
J'ai un violon ici qui montre comment restaurer la position de défilement dans la vue liste après une vue de détail; pas encore encapsulé dans une directive, travaillant dessus ...
$scope.scrollPos = {}; // scroll position of each view
$(window).on('scroll', function() {
if ($scope.okSaveScroll) { // false between $routeChangeStart and $routeChangeSuccess
$scope.scrollPos[$location.path()] = $(window).scrollTop();
//console.log($scope.scrollPos);
}
});
$scope.scrollClear = function(path) {
$scope.scrollPos[path] = 0;
}
$scope.$on('$routeChangeStart', function() {
$scope.okSaveScroll = false;
});
$scope.$on('$routeChangeSuccess', function() {
$timeout(function() { // wait for DOM, then restore scroll position
$(window).scrollTop($scope.scrollPos[$location.path()] ? $scope.scrollPos[$location.path()] : 0);
$scope.okSaveScroll = true;
}, 0);
});
Le violon montre également qu'il faut aller chercher la liste une fois, en dehors de 'ListCtrl'.
Vous trouverez ci-dessous une autre version de la directive keep-scroll-pos. Cette version
Se souvient de la position de défilement de chaque templateUrl de votre définition $ routeProvider.
Respecte les balises de hachage, par exemple, #/home # section-2 , passera à # section-2 / pas la position de défilement précédente.
Il est facile à utiliser car il est autonome et stocke les positions de défilement en interne.
Exemple d'utilisation de HTML:
<div ng-view keep-scroll-pos></div>
Le code de la directive keepScrollPos est ci-dessous:
"use strict"; angular.module ("myApp.directives", []) . directive ("keepScrollPos", function ($ route, $ fenêtre, $ timeout) , $ location, $ anchorScroll) { // cache position de défilement du templateUrl var. scrollPosCache de chaque route = {}; // fonction de compilation fonction de retour (portée, élément, attrs) { scope. $ on ('$ routeChangeStart', function () { // stocke la position de défilement de la vue actuelle if ($ route.current) { scrollPosCache [$ route.current.loadedTemplateUrl] = [$ window.pageXOffset, $ window.pageYOffset]; } }). () { // si le hachage est spécifié explicitement, il remplace la position de défilement précédemment enregistrée .__ if ($ location.hash ()) { $ anchorScroll (); // else obtenir la position de défilement précédente, sinon, défiler vers le haut de la page } else { var prevScrollPos = scrollPosCache [$ route.current.loadedTemplateUrl] || [0, 0]; $ timeout (function () { $ window.scrollTo (prevScrollPos [0], prevScrollPos [1]); }, 0); } }); } });
Pour ignorer la position de défilement précédemment stockée et pour forcer le défilement vers le haut, utilisez la pseudo balise de hachage: #top , par exemple, href = " #/home # top ".
Sinon, si vous préférez toujours faire défiler l'écran jusqu'en haut, utilisez la commande intégrée ng-view autoscroll :
<div ng-view autoscroll></div>
J'ai utilisé la solution de @ Joseph Oster afin de créer une directive .
comme les autres événements sont obsolètes.
Fiddle is here: http://jsfiddle.net/empie/p5pn3rvL/
Source de la directive:
angular.module('myapp', ['ngRoute'])
.directive('autoScroll', function ($document, $timeout, $location) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.okSaveScroll = true;
scope.scrollPos = {};
$document.bind('scroll', function () {
if (scope.okSaveScroll) {
scope.scrollPos[$location.path()] = $(window).scrollTop();
}
});
scope.scrollClear = function (path) {
scope.scrollPos[path] = 0;
};
scope.$on('$locationChangeSuccess', function (route) {
$timeout(function () {
$(window).scrollTop(scope.scrollPos[$location.path()] ? scope.scrollPos[$location.path()] : 0);
scope.okSaveScroll = true;
}, 0);
});
scope.$on('$locationChangeStart', function (event) {
scope.okSaveScroll = false;
});
}
};
})
j'ai créé une directive qui fonctionne sur le défilement de la fenêtre (elle pourrait être mise à jour pour fonctionner sur n'importe quel élément)
utilisation du html
<div ng-keep-scroll="service.scrollY">
<!-- list of scrolling things here -->
</div>
où "service.scrollY" DOIT être une variable dans un service. Les services conservent leur état et leurs valeurs, les contrôleurs sont recréés à chaque chargement et effacent leurs valeurs afin que vous ne puissiez pas les utiliser pour stocker des données persistantes. le contrôleur a une variable d’étendue pointant vers le service.
directive js
app.directive('ngKeepScroll', function ($timeout) {
return function (scope, element, attrs) {
//load scroll position after everything has rendered
$timeout(function () {
var scrollY = parseInt(scope.$eval(attrs.ngKeepScroll));
$(window).scrollTop(scrollY ? scrollY : 0);
}, 0);
//save scroll position on change
scope.$on("$routeChangeStart", function () {
scope.$eval(attrs.ngKeepScroll + " = " + $(window).scrollTop());
});
}
});
Basé sur l'excellente réponse de br2000, j'ai mis à jour le code de la directive pour qu'il fonctionne avec ui-router. Pour les états ayant le même nom mais des paramètres différents, je sérialise l'objet $ state.params pour créer une clé unique dans l'objet scrollPosCache
.
.directive("keepScrollPos", function($state, $window, $timeout, $location, $anchorScroll) {
// cache scroll position of each route's templateUrl
var scrollPosCache = {};
// compile function
return function(scope, element, attrs) {
scope.$on('$stateChangeStart', function() {
// store scroll position for the current view
if ($state.current.name) {
scrollPosCache[$state.current.name + JSON.stringify($state.params)] = [ $window.pageXOffset, $window.pageYOffset ];
}
});
scope.$on('$stateChangeSuccess', function() {
// if hash is specified explicitly, it trumps previously stored scroll position
if ($location.hash()) {
$anchorScroll();
// else get previous scroll position; if none, scroll to the top of the page
} else {
var prevScrollPos = scrollPosCache[$state.current.name + JSON.stringify($state.params)] || [ 0, 0 ];
$timeout(function() {
$window.scrollTo(prevScrollPos[0], prevScrollPos[1]);
}, 0);
}
});
}
})
J'ai créé une version qui fonctionne avec n'importe quel élément débordé, pas seulement le corps du document:
.directive("keepScrollPos", function($route, $timeout, $location, $anchorScroll) {
// cache scroll position of each route's templateUrl
var cache = {};
return {
restrict : 'A',
link: function($scope, elements, attrs){
$scope.$on('$routeChangeStart', function() {
// store scroll position for the current view
if($route.current)
cache[$route.current.loadedTemplateUrl + ':' + attrs.keepScrollPos] = [elements[0].scrollLeft, elements[0].scrollTop];
});
$scope.$on('$routeChangeSuccess', function(){
// if hash is specified explicitly, it trumps previously stored scroll position
if($location.hash()){
$anchorScroll();
return;
}
// else get previous scroll position and apply it if it exists
var pos = cache[$route.current.loadedTemplateUrl + ':' + attrs.keepScrollPos];
if(!pos)
return;
$timeout(function(){
elements[0].scrollLeft = pos[0];
elements[0].scrollTop = pos[1];
}, 0);
});
}
}
})
Utilisez-le comme:
<div keep-scroll-pos="some-identifier"> ... </div>
Si votre page nécessite l'extraction de données à afficher, vous devrez peut-être utiliser $ routeChangeSuccess et retarder l'appel de fonction de défilement.
scope.$on("$routeChangeSuccess", function() {
$timeout(function () {
var scrollY = parseInt(scope.$eval(attrs.ngKeepScroll));
$(window).scrollTop(scrollY ? scrollY : 0);
}, 1000); // delay by 1 sec
});
cela peut résoudre votre problème, cela fonctionne pour moi $httpProvider.defaults.cache = true;
Pour ceux d'entre vous qui sont allés avec la réponse emp, mais utilisaient un routeur angulaire angulaire> = version 1.0.0 (version 1.0.3 actuelle), veuillez voir sa directive réécrite, en utilisant les nouvelles transitions des routeurs ui.
HTML
<div ui-view keep-scroll-pos></div>
Directive angulaire
angular.module("app")
.directive("keepScrollPos", function($transitions, $state, $window, $timeout, $location, $anchorScroll) {
// cache scroll position of each state's templateUrl
var scrollPosCache = {};
return {
link: function(scope, element, attrs) {
$transitions.onStart({ }, function( trans ) {
// store scroll position for the current view
if (trans.from().name) {
scrollPosCache[trans.from().templateUrl] = [ $window.pageXOffset, $window.pageYOffset ];
}
trans.promise.finally(function () {
// if hash is specified explicitly, it trumps previously stored scroll position
if ($location.hash()) {
$anchorScroll();
// else get previous scroll position; if none, scroll to the top of the page
} else {
var prevScrollPos = scrollPosCache[trans.to().templateUrl] || [ 0, 0 ];
$timeout(function() {
$window.scrollTo(prevScrollPos[0], prevScrollPos[1]);
}, 200);
}
});
});
}
}
});
J'utilise une solution personnalisée dans mon projet.
Étape 1: Obtenez la position du clic sur la liste et enregistrez-la dans la mémoire de stockage locale.
var position = document.body.scrollTop;
localStorage.setItem("scrollPosition",position);
Étape 2: dans la vue détaillée, définissez la variable globale backFromDetailView sur true.
backFromDetailView = true;
Étape 3: En revenant de la page de vue détaillée vers la liste. Tout le contenu est rechargé à partir du serveur à la position défilée.
Pour cela, liez une fonction dans le code HTML à l'aide de la ligne suivante:
Et le contrôleur contient la fonction:
$scope.goto = function (){
if(backFromDetailView){
window.scrollTo(0, localStorage.getItem("scrollPosition"));
}
}
Quelques inconvénients de cette technique:
Tout le contenu, y compris le contenu supplémentaire, est rechargé à nouveau.
Sous iOS, un écran noir apparaît avant le défilement vers la position appropriée
J'ai trouvé un autre moyen simple de résoudre ce problème:
var scrollValue = $(window).scrollTop();
$rootScope.$on("$routeChangeStart", function() {
scrollValue = $(window).scrollTop();
});
$rootScope.$on('$routeChangeSuccess', function(newRoute, oldRoute) {
setTimeout(function() { $(window).scrollTop(scrollValue); }, 0);
});
Il suffit de le mettre dans .run ().
De cette façon, définir la valeur de délai d’attente sur 0 fonctionne toujours, mais s’exécute après le rendu de la page (sans la fonction de délai d’expiration, elle est exécutée avant le contenu (modèle ou chargement de données), ce qui rend la fonction inutile.
Si vous récupérez des données à partir d'une API, vous pouvez envelopper le délai dans une fonction de $ rootScope et l'exécuter après une requête réussie.
Vous devez réinitialiser la position de défilement à chaque changement d’itinéraire . Utilisez ceci dans votre AppController principal:
$scope.$on("$routeChangeSuccess", function () {
$anchorScroll();
});
Ou si vous utilisez ui-route:
$scope.$on("$stateChangeSuccess", function () {
$anchorScroll();
});
Pour plus d'informations, voir Dans AngularJS, comment puis-je ajouter une surveillance $ sur le hachage de l'URL?
Contrairement aux autres réponses, je voulais retenir plus que des rouleaux, à savoir input
field value
s.
Non seulement cela, mais beaucoup d'entre eux ont supposé
body
comme élément de défilement (par exemple, si vous utilisez capture angulaire ?),ng-view
).<body> <!-- doesn't scroll -->
<div snap-drawers>..</div>
<div snap-content="" history="scrollTop"> <!-- the scrolling div-->
<header>...</header>
<div ng-view>
<input name="email" history="val"> <!-- tag with value we want remembered -->
<div history="scrollLeft" history-watch="scroll" id="evenHorizontalScroll"><!--
custom value we want remembered.
NB it must have an id to be identified after angular
removes it from the DOM between views,
and since it is not a recognised default we can tell my
directive the jquery event function what to watch
--></div>
</div>
</div>
</body>
J'ai écrit une directive [malheureusement beaucoup plus longue] de portée partagée qui prend en charge ces problèmes.
.directive('history', function($compile, $rootScope, $location) {
return {
restrict : 'A',
replace : false,
scope : false,
controller : function($scope, $timeout) {
//holds all the visited views
var states = new Object();
//the current view
var state = null;
//how many names have been generated where the element itself was used
var generated = 0;
//logs events if allowed
function debug(from) {
//comment this to watch it working
//return;
console.log('StateHistory: ' + from);
if (from == 'went')
console.log(state);
}
//applies the remembered state
function apply() {
var element;
//for each item remembered in the state
for (var query in state) {
//use the element directly, otherwise search for it
(state[query].element || $(query))
//use the appropriate function
[state[query].property](
//and set the value
state[query].value
)
;
debug('applying:' + query + ':' + state[query].value);
}
//start recording what the user does from this point onward
$scope.ignore = false;
}
//generates a reference we can use as a map key
$scope.generateRef = function() {
return '' + (++generated);
};
//views changed
$scope.went = function() {
debug('went');
//set the current state
state = states[$location.path()];
//if we dont remember the state of the page for this view
if (!state)
//get recording!
state = states[$location.path()] = new Object();
//apply the state after other directives
//(like anchorScroll + autoscroll) have done their thing
$timeout(apply);
};
//one of the elements we're watching has changed
$scope.changed = function(name, element, property, useObject) {
//if we're not meant to be watching right now
//i.e. if the user isnt the one changing it
if ($scope.ignore) {
debug('ignored');
return;
}
//if we havent recorded anything for this here yet
if (!state[name]) {
//start recording
state[name] = {property:property};
//and remember to leave behind a reference if the name isn't
//good enough (was generated)
if (useObject)
state[name].element = element;
}
//use the requested function to pull the value
state[name].value = element[property]();
debug('changed:' + name + ':' + state[name].value);
};
//initial view
$scope.went();
//subsequent views
$rootScope.$on('$routeChangeSuccess', $scope.went);
$rootScope.$on('$routeChangeError', $scope.went);
$rootScope.$on('$routeChangeStart', function() {
debug('ignoring');
$scope.ignore = true;
});
},
link: function (scope, element, attrs) {
//jquery event function name
var watch = attrs.historyWatch;
//if not set, use these defaults
if (!watch) {
switch (attrs.history) {
case 'val':
watch = 'change';
break;
case 'scrollTop':
watch = 'scroll';
break;
default:
watch = attrs.history;
}
}
//the css selector to re-find the element on view change
var query = null;
//the reference to the state remembered
var name;
//try using the id
if (attrs.id)
name = query = '#' + attrs.id;
//try using the form name
else if (attrs.name)
name = query = '[name=' + attrs.name + ']';
//otherwise we'll need to just reference the element directly
//NB should only be used for elements not swapped out by angular on view change,
//ie nothing within the view. Eg the view itself, to remember scrolling?
else
name = scope.generateRef();
//jquery value function name
var property = attrs.history;
//watch this element from here on out
element.on(watch, function() {
scope.changed(name, element, property, !query);
});
}
};
})
Excellente solution de @ br2000.
Cependant, malheureusement, la page sur laquelle je revenais chargeait toujours les données du backend vers une longue liste lorsque la directive tentait de restaurer la position.
Donc, évidemment, la position de défilement n'a pas été restaurée. Je l'ai résolu en utilisant $interval
au lieu de $timeout
et lui ai donné 20 répétitions avec 300ms timeout
. J'ai stocké la promesse retournée de $interval
et ensuite vérifié dans la fonction $interval
si la position actuelle est maintenant identique à la position stockée et si oui, j'appelle une méthode de portée qui annule la fonction $interval - $interval.cancel(promise).
.
De plus, mes pageYOffset
et pageXOffset
étaient toujours à 0, car overflow-x: hidden
a été appliqué à la racine div
dans la DOM
. Je l'ai résolu en enveloppant la racine div
dans une autre div
sur laquelle j'ai ensuite placé cette directive.