示例应用:http://angular.github.com/angular-phonecat/step-11/app/#/phones
如果您选择最后一部手机“摩托罗拉魅力”,它会显示手机的详细信息。 当您在浏览器上导航回来时,它会重新加载数据,滚动位于顶部。
当导航回来时,自动滚动到哪里的最佳方法是什么? 而且,为什么angular会重新加载数据?
我的计算机上有相同的“angular-phonecat”样本,我添加了一个无限滚动,滚动时会加载更多数据。所以我真的不希望用户再次重新加载50多个项目或向下滚动30秒。
答案 0 :(得分:31)
我这里有一个小提琴,展示了如何在详细视图后恢复列表视图中的滚动位置;尚未封装在指令中,正在努力......
$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”之外获取列表一次。
答案 1 :(得分:17)
以下是keep-scroll-pos指令的另一个版本。这个版本
记住$ routeProvider定义的每个 templateUrl 的滚动位置。
尊重哈希标签,例如#/ home# section-2 ,将滚动到#section-2 而不是之前的滚动位置。
易于使用,因为它是自包含的,并且在内部存储滚动位置。
html使用示例:
<div ng-view keep-scroll-pos></div>
keepScrollPos指令的代码如下:
"use strict"; angular.module("myApp.directives", []) .directive("keepScrollPos", function($route, $window, $timeout, $location, $anchorScroll) { // cache scroll position of each route's templateUrl var scrollPosCache = {}; // compile function return function(scope, element, attrs) { scope.$on('$routeChangeStart', function() { // store scroll position for the current view if ($route.current) { scrollPosCache[$route.current.loadedTemplateUrl] = [ $window.pageXOffset, $window.pageYOffset ]; } }); scope.$on('$routeChangeSuccess', 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[$route.current.loadedTemplateUrl] || [ 0, 0 ]; $timeout(function() { $window.scrollTo(prevScrollPos[0], prevScrollPos[1]); }, 0); } }); } });
要忽略以前存储的滚动位置,并强制滚动到顶部,请使用伪哈希标记: #top ,例如,href =“#/ home #top ”
或者,如果您希望始终滚动到顶部,请使用内置的ng-view autoscroll 选项:
<div ng-view autoscroll></div>
答案 2 :(得分:9)
我使用@Joseph Oster的解决方案来创建指令。 我也冒昧地更新了使用的答案:
因为其他事件已经过时了。
小提琴在这里: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;
});
}
};
})
答案 3 :(得分:6)
之前我没有使用它,但angular有$anchorScroll服务。至于重新加载数据,您可以使用$cacheFactory对其进行缓存,或将数据存储在更高的范围内。
答案 4 :(得分:5)
我创建了一个适用于窗口滚动的指令(它可以更新为可以处理任何元素)
html用法
<div ng-keep-scroll="service.scrollY">
<!-- list of scrolling things here -->
</div>
其中“service.scrollY”必须是服务中的变量。服务保留其状态和值,每次加载和清除其值时都会重新创建控制器,因此您无法使用它们来存储持久数据。控制器有一个指向服务的范围变量。
指令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());
});
}
});
答案 5 :(得分:3)
基于br2000的优秀答案,我更新了指令代码以使用ui-router。对于具有相同名称但不同params的状态,我将$ 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);
}
});
}
})
答案 6 :(得分:1)
如果您的页面需要提取要显示的数据,则可能必须使用$ routeChangeSuccess并延迟滚动函数调用。
scope.$on("$routeChangeSuccess", function() {
$timeout(function () {
var scrollY = parseInt(scope.$eval(attrs.ngKeepScroll));
$(window).scrollTop(scrollY ? scrollY : 0);
}, 1000); // delay by 1 sec
});
答案 7 :(得分:1)
我制作了一个适用于任何溢出元素的版本,而不仅仅是文档正文:
.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>
答案 8 :(得分:0)
我找到了解决此问题的另一种简单方法:
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它仍然有效,但在页面呈现后运行(没有超时功能,它在呈现内容(即模板或数据加载)之前运行,使得该功能无用。
如果从某个API获取数据,可以将超时包装在$ rootScope中的函数中,并在成功请求后运行它。
答案 9 :(得分:0)
您需要在每次路线更改时重置滚动位置。 在主AppController中使用它:
$scope.$on("$routeChangeSuccess", function () {
$anchorScroll();
});
或者如果您使用的是ui-route:
$scope.$on("$stateChangeSuccess", function () {
$anchorScroll();
});
有关详细信息,请参阅In AngularJS, how do I add a $watch on the URL hash?
答案 10 :(得分:0)
这可能会解决您的问题,它对我有用
$httpProvider.defaults.cache = true;
答案 11 :(得分:0)
与其他答案不同,我想记住的不仅仅是滚动,即input
字段value
s。
不仅如此,还有很多人假设
body
作为滚动元素(例如,如果您使用angular snap会怎么样?),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
//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);
});
}
};
})
答案 12 :(得分:0)
我在项目中使用自定义解决方案。
步骤1:获取列表上的点击位置并将其保存在本地存储中。
var position = document.body.scrollTop;
localStorage.setItem("scrollPosition",position);
步骤2:在详细视图中,将全局变量backFromDetailView设置为true。
backFromDetailView = true;
步骤3:从详细视图页面返回到列表。所有内容再次从服务器重新加载到滚动位置。
为此,使用以下行绑定html中的函数:
控制器包含以下功能:
$scope.goto = function (){
if(backFromDetailView){
window.scrollTo(0, localStorage.getItem("scrollPosition"));
}
}
这种技术的一些缺点:
重新加载包含其他内容的所有内容。
在iOS中,在滚动到适当的位置之前会出现黑屏 位置。
答案 13 :(得分:0)
@ br2000的绝佳解决方案。
然而不幸的是,我正在滚动回来的页面,当指令试图恢复位置时,仍在从后端加载数据到长列表。
很明显,它无法恢复滚动位置。我使用$interval
代替$timeout
解决了问题,并使用300ms timeout
重复了20次。我存储了从$interval
返回的promise,然后在$interval
函数内检查当前位置是否与存储位置相同,如果是,我调用取消$interval - $interval.cancel(promise).
<的范围方法/ p>
此外,最初我的pageYOffset
和pageXOffset
始终为0,因为overflow-x: hidden
已应用于div
中的根DOM
。我通过将根div
包装在另一个div
中来解决它,然后我放置了这个指令。
答案 14 :(得分:0)
对于那些使用emp的答案,但使用的是角度ui-router&gt; =版本1.0.0(当前1.0.3)的人,请使用ui-routers新转换来查看他的指令。
HTML
<div ui-view keep-scroll-pos></div>
Angular Directive
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);
}
});
});
}
}
});