保留在AngularJS路线变化的滚动位置?

示例应用程序: http : //angular.github.com/angular-phonecat/step-11/app/#/phones

如果你select了最后一部手机“摩托罗拉魅力”,它会告诉你手机的细节。 当你浏览器浏览器重新加载数据和滚动是在顶部。

导航回来后,自动滚动到哪里的最好方法是什么? 而且,为什么angular度重新加载数据?

我的计算机上有相同的“angular手机”示例,我添加了一个无限滚动,滚动时会加载更多的数据。 所以我真的不希望用户重新加载50+项目或向下滚动30秒。

我以前没有用过,但是angular有一个$ anchorScroll服务。 至于重新加载数据,您可以使用$ cacheFactorycaching它,或者将数据存储在更高的范围内。

在这里我有一个小提琴,它显示了如何在详细视图之后在列表视图中恢复滚动位置。 没有封装在一个指令中,正在处理…

http://jsfiddle.net/BkXyQ/6/

$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); }); 

小提琴还显示一次,“ListCtrl”之外的列表。

下面是keep-scroll-pos指令的另一个版本。 这个版本

  • 记住$ routeProvider定义的每个templateUrl的滚动位置。

  • 尊重散列标签,例如#/ home# section-2 ,将滚动到#section-2而不是之前的滚动位置。

  • 易于使用,因为它是独立的,并在内部存储滚动位置。

使用html的例子:

 <div ng-view keep-scroll-pos></div> 

keepScrollPos指令的代码如下:

 “严格使用”;

 angular.module(“myApp.directives”,[])

 .directive(“keepScrollPos”,函数($ route,$ window,$ timeout,$ location,$ anchorScroll){

     //caching每个路由的templateUrl的滚动位置
     var scrollPosCache = {};

     //编译函数
    返回函数(范围,元素,attrs){

         $ on('$ routeChangeStart',function(){
             //存储当前视图的滚动位置
             if($ route.current){
                 scrollPosCache [$ route.current.loadedTemplateUrl] = [$ window.pageXOffset,$ window.pageYOffset];
             }
         });

         $ on('$ routeChangeSuccess',function(){
             //如果哈希是明确指定的,则胜过先前存储的滚动位置
             if($ location.hash()){
                 $ anchorScroll();

             //否则获取上一个滚动位置; 如果没有,请滚动到页面顶部
             } else {
                 var prevScrollPos = scrollPosCache [$ route.current.loadedTemplateUrl] ||  [0,0];
                 $ timeout(function(){
                     $ window.scrollTo(prevScrollPos [0],prevScrollPos [1]);
                 },0);
             }
         });
     }
 });

要忽略先前存储的滚动位置,并强制滚动到顶部,请使用伪哈希标记: #top ,例如href =“ #/ home#top ”。

或者,如果您只想总是滚动到顶部,请使用内置的ng-view 自动滚动选项:

 <div ng-view autoscroll></div> 

我已经使用@Joseph Oster的解决scheme来创build一个指令。 我也自由地更新了使用的答案:

  • $ locationChangeStart
  • $ locationChangeSuccess

因为其他事件已经过时了。

小提琴在这里: http : //jsfiddle.net/empie/p5pn3rvL/

指令来源:

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

我创build了一个指令,在窗口滚动(它可以更新为任何元素上工作,虽然)

使用html

 <div ng-keep-scroll="service.scrollY"> <!-- list of scrolling things here --> </div> 

其中“service.scrollY”必须是服务中的variables。 服务保留它们的状态和值,控制器在每次加载时都被重新创build并清除它们的值,所以你不能使用它们来存储持久数据。 控制器有一个指向该服务的范围variables。

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

基于来自br2000的伟大答案,我更新了指令代码与ui-router一起工作。 对于具有相同名称但不同参数的状态,我序列化$ state.params对象以构成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); } }); } }) 

如果您的页面需要获取要显示的数据,则可能需要使用$ routeChangeSuccess并延迟滚动function调用。

  scope.$on("$routeChangeSuccess", function() { $timeout(function () { var scrollY = parseInt(scope.$eval(attrs.ngKeepScroll)); $(window).scrollTop(scrollY ? scrollY : 0); }, 1000); // delay by 1 sec }); 

我做了一个适用于任何溢出元素的版本,而不仅仅是文档体:

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

像这样使用它:

 <div keep-scroll-pos="some-identifier"> ... </div> 

我发现了另一个简单的方法来解决这个问题:

 var scrollValue = $(window).scrollTop(); $rootScope.$on("$routeChangeStart", function() { scrollValue = $(window).scrollTop(); }); $rootScope.$on('$routeChangeSuccess', function(newRoute, oldRoute) { setTimeout(function() { $(window).scrollTop(scrollValue); }, 0); }); 

只要把它放在.run()。

这样,将超时值设置为0仍然有效,但是在页面呈现之后运行(没有超时function,它在呈现内容(即模板或数据加载)之前运行,使得该function无用。

如果您从某个API获取数据,则可以将超时值包装到$ rootScope中的函数中,并在请求成功后运行。

您需要重置每个路线更改的滚动位置。 在你的主AppController中使用这个:

  $scope.$on("$routeChangeSuccess", function () { $anchorScroll(); }); 

或者如果你正在使用ui-route:

  $scope.$on("$stateChangeSuccess", function () { $anchorScroll(); }); 

有关更多信息,请参阅在AngularJS中,如何在URL哈希上添加$ watch?

这可能会解决你的问题,它适用于我$httpProvider.defaults.cache = true;

与其他答案不同,我想要记住的不仅仅是滚动条​​,即input字段value

不仅如此,而且他们中的很多人都假设了

  • 你只想记住一个滚动元素(也许你有窗格或其他应用程序的显示),
  • 你有body作为你的滚动元素(例如,如果你正在使用angular度捕捉 ?),
  • 或者你的滚动元素不能被angular度replace(即在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> 

我已经写了一个不幸的更长的共享范围指令来处理这些问题。

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

我在我的项目中使用自定义解决scheme。

第一步:获取列表中点击的位置并保存到本地存储。

 var position = document.body.scrollTop; localStorage.setItem("scrollPosition",position); 

第2步:在详细视图中将全局variablesbackFromDetailView设置为true。

 backFromDetailView = true; 

第3步:从详细视图页返回列表。 所有的内容再次从服务器重新加载到滚动的位置。

为此,使用以下行绑定html中的函数:

控制器包含以下function:

 $scope.goto = function (){ if(backFromDetailView){ window.scrollTo(0, localStorage.getItem("scrollPosition")); } } 

这种技术的一些缺点:

  1. 包含附加内容的所有内容都会重新加载。

  2. 在iOS中,在滚动到适当的位置之前出现黑屏。

由@ br2000大的解决scheme。

无论如何,不​​幸的是我的页面,我正在滚动回来,还是加载数据从后端到一个长长的列表,当指令试图恢复的立场。

所以显然它没有恢复滚动位置。 我使用$interval而不是$timeout解决了这个问题,并在300ms timeout重复了20次。 我存储了从$interval返回的promise,然后在$interval函数内部检查,如果当前位置现在与存储位置相同,如果是,则调用取消$interval - $interval.cancel(promise).的作用域方法$interval - $interval.cancel(promise).

另外,最初我的pageYOffsetpageXOffset总是0,因为overflow-x: hidden被应用于DOM的根div 。 我解决了这个问题,把root的div放在另一个div里面,然后我放了这个指令。