やりたいこと
コレクションをng-repeat
で描画しているときに、そのコレクションに対してunshift
(先頭に挿入)した場合、DOM描画後にスクロール位置が先頭に移動してしまいます。これを回避したいという話です。
解決方法
厳密に言うと、「元のスクロール位置を保持」しているからこそ、先頭に移動してしまうわけで。。。つまり、もともとスクロール位置が画面トップだった場合はscrollTop == 0
ですが、unshift
による再描画後もscrollTop == 0
のままなので、スクロール位置が先頭に来ているというわけです。
というわけで、再描画後にスクロールコンテナの高さが増えた分だけ下にスクロールさせてやればよい、ということになります。(ベストな方法かは分からないですが・・・)
サンプル
初期状態ではitems
というコレクションを100件表示します。画面上部の"Load Previous Items"というリンクをクリックすれば、その前にさらに100件のitem
を読み込みます。その際にスクロール位置を保持するというサンプルです。
<!doctype html>
<html ng-app="app">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="ItemsCtrl" keep-scroll-position>
<a href="" ng-click="loadPreviousItems()" ng-if="!loaded">Load Previous Items</a>
<div ng-repeat="item in items">
<div>{{item}}</div>
</div>
</body>
</html>
angular.module('app', [])
.controller('ItemsCtrl', function($scope) {
$scope.items = [];
for (var i=100; i < 200; i++) {
$scope.items.push('item' + i);
}
$scope.loadPreviousItems = function() {
var previousItems = [];
for (var i=0; i < 100; i++) {
previousItems.push('item' + i);
}
Array.prototype.unshift.apply($scope.items, previousItems);
$scope.loaded = true;
};
})
.directive('keepScrollPosition', function() {
return function(scope, el, attrs) {
scope.$watch(
function() { return el[0].clientHeight; },
function(newHeight, oldHeight) {
console.debug('Height was changed', oldHeight, newHeight);
el[0].scrollTop = newHeight - oldHeight;
});
};
});
ソース解説
keepScrollPosition
というディレクティブにスクロールコンテナ(この場合はbody)の高さを監視してます。高さに変更があったら(=items
が追加された)スクロール位置を元の場所まで移動してやります。
_
できてみると、めちゃくちゃ単純なコードなんですが、「高さを監視する」という発想に至るまでかなり時間を要してしまいました・・・。