How to retain scroll position of ng-repeat in AngularJS?

若如初见. 提交于 2019-11-27 18:46:06

It maybe solved quite elegantly, by using scrollTop property of div. I used two directives - one handles scroll position of the wrapper element, the other register new elements. Give me a shout if anything unclear.

DEMO

JS:

.directive("keepScroll", function(){

  return {

    controller : function($scope){
      var element = null;

      this.setElement = function(el){
        element = el;
      }

      this.addItem = function(item){
        console.log("Adding item", item, item.clientHeight);
        element.scrollTop = (element.scrollTop+item.clientHeight+1);
       //1px for margin from your css (surely it would be possible
       // to make it more generic, rather then hard-coding the value)
      };

    },

    link : function(scope,el,attr, ctrl) {

     ctrl.setElement(el[0]);

    }

  };

})

.directive("scrollItem", function(){

  return{
    require : "^keepScroll",
    link : function(scope, el, att, scrCtrl){
      scrCtrl.addItem(el[0]);
    }
  }
})

HTML:

<div class="wrapper" keep-scroll>
  <div class="item" scroll-item ng-repeat="item in items  | orderBy: '-id'">
    {{ item.name }}
   </div>
</div>

You know other people are trying to solve this problem using a different approach in terms of UI. They don't just POP new items on top, but instead they show a small clickable link on top stating how many new items are added since he last checked it.

[2 new items, Click here to refresh]

item 5
item 4
item 3

Check out how twitter is solving this.

I know it's a bit contradicting with want you want, but perhaps this is better in terms of UX? User wants to know if there are new items coming in.

You could scroll by the amount of the height of the added elements

$scope.addNewItem = function() {
    var wrapper = document.getElementsByClassName('wrapper')[0];

    $scope.items = $scope.items.concat({
      id: $scope.items.length,
      name: "item " + $scope.items.length
    });
    // will fail if you observe the item 0 because we scroll before the view is updated;
    wrapper.scrollTop+=101; //height+margings+paddings
  };

I am using a bad practice of accessing the DOM from the controller. A more modular approach would be to create a directive which will handle all cases and change the scroll position after the view is updated.

Demo at http://jsbin.com/zofofapo/8/edit


Alternatively, for the case where the items are not equally high, you could see how much scroll is left before the insertion, and re-set it after the insertion

$scope.addNewItem = function() {
    var wrapper = document.getElementsByClassName('wrapper')[0],
        scrollRemaining = wrapper.scrollHeight - wrapper.scrollTop;

    $scope.items = $scope.items.concat({
      id: $scope.items.length,
      name: "item " + $scope.items.length
    });
    // will fail if you observe the item 0 because we scroll before the view is updated;
    $timeout(function(){
      wrapper.scrollTop = wrapper.scrollHeight - scrollRemaining;
    },0);
  };

Demo at http://jsbin.com/zofofapo/9/edit

Below is an improvement to Arthur's version that prevents scrolling regardless if the added item is added above or below the scroll: JS Bin

angular.module("Demo", [])

.controller("DemoCtrl", function($scope) {
  $scope.items = [];
  
  for (var i = 0; i < 10; i++) {
    $scope.items[i] = {
      id: i,
      name: 'item ' + i
    };
  }
  
  $scope.addNewItemTop = function() {
    $scope.items.unshift({
      id: $scope.items.length,
      name: "item " + $scope.items.length
    });
  };
  
  $scope.addNewItemMiddle = function() {
    $scope.items.splice(5, 0, {
      id: $scope.items.length,
      name: "item " + $scope.items.length
    });
  };
  
  $scope.addNewItemBottom = function() {
    $scope.items = $scope.items.concat({
      id: $scope.items.length,
      name: "item " + $scope.items.length
    });
  };
})

.directive("keepScroll", function(){
  
  return {

    controller : function($scope){
      var element = 0;
      
      this.setElement = function(el){
        element = el;
      };

      this.addItem = function(item){
        console.group("Item");
        console.log("OffsetTop: " + item.offsetTop);
        console.log("ScrollTop: " + element.scrollTop);
        
        if(item.offsetTop <= element.scrollTop) {
          console.log("Adjusting scorlltop position");
          element.scrollTop = (element.scrollTop+item.clientHeight+1); //1px for margin
        } else {
          console.log("Not adjusting scroll");
        }
        console.groupEnd("Item");
      };
      
    },
    
    link : function(scope,el,attr, ctrl) {
      
     ctrl.setElement(el[0]);
      
    }
    
  };
  
})

.directive("scrollItem", function(){
  
  
  return{
    require : "^keepScroll",
    link : function(scope, el, att, scrCtrl){
      scrCtrl.addItem(el[0]);
    }
  };
});
.wrapper {
  width: 200px;
  height: 300px;
  border: 1px solid black;
  overflow: auto;
  /* Required for correct offsetParent */
  position: relative; 
}
.item {
  background-color: #ccc;
  height: 100px;
  margin-bottom: 1px;
}
<!DOCTYPE html>
<html>
<head>
<script src="//code.angularjs.org/1.3.0-beta.7/angular.js"></script>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body ng-app="Demo" ng-controller="DemoCtrl">
  <div class="wrapper" keep-scroll>
    <div class="item" scroll-item ng-repeat="item in items">
      {{ item.name }}
    </div>
  </div>
  <button ng-click="addNewItemTop()">
    Add New Item Top
  </button>
  <button ng-click="addNewItemMiddle()">
    Add New Item Middle
  </button>
  <button ng-click="addNewItemBottom()">
    Add New Item Bottom
  </button>
</body>
</html>

You could defer adding the items until the user scrolls the top of the list into view. There is no point rendering the items before.

It could look like this (perhaps with an added animation).

The FaceBook Way: Everyone is suggesting this, here is a pseudo-implementation:

MYEXAMPLE

As new objects are added, pop them into a "More Queue".

  <div style="height:15px">
      <button  ng-if="moreQueue"ng-click="transferWait()"> See More {{ moreQueue }}
      </button >
    </div>  
  <div class="wrapper">
    <div class="item" ng-repeat="item in items | orderBy: '-id'">
      {{ item.name }}
    </div>
  </div>

MessageHandlerController will have at least 2 arrays (we should treat as queue's b/c we'll pop from bottom, up.

  • Active Messages
  • Waiting Messages

As your Signal R/Service Bus populates your WaitingQueue, your ng-if renders increases in size and your $scope.SizeOfWaitingQueue=SizeOfWaitingQueue(). This re-assigning process should happen every iteration so you don't have to dirty check your More size Repo

You need to add a scrollspy directive to your container that updates its position on every user scroll, and gets notified on every repeat render so it can reposition it self to its saved state. your html might look like this

<div scrollspy id="myscrollspy">
     <ul>
       <li ng-repeat="" notifyscroll></li>
     </ul>
</div>

the scroll spy would have the required css overflow settings and scroll-x or scroll-y to keep track of the current scroll and avoid polluting the scope it should also watch for an event comming from the ng-repeat that tells him a change occured and should set the scrool.

ng-repeat could notify by attaching a new directive notify scroll that launches an event. not sure if curretn version of angular supports postrender event.

the way to position the scroll will depend on whether you are using a 3rd party library $.scrollTop(pos) or no. this will do it or should. hope it helps

NimChimpsky

There are, I believe, only a few possible solutions

1) Don't add the item (as per the other answer)

2) Add the item at bottom, so the list doesn't move.

3) Add the item at top and scroll the screen automatically so that the new item's height is accounted for, and everything is kept seemingly as before. The list will move down, but the viewable screen itself will also move - so relatively nothing will be seen to move. Well other elements that are not part of list will, but that might actually look quite nice...

You can solve this problem with ng-animate:

.animation('.keep-scroll', [function () {
    var keepScroll = function(element, leave){
        var elementPos = element.offset().top;
        var scrollPos = document.body.scrollTop;

        if(elementPos < scrollPos){
            var height = element[0].clientHeight;
            if(leave){
                height *= (-1);
            }
            document.body.scrollTop += height;
        }
    };

    return {
        enter: function (element, doneFn) {
            keepScroll(element);
            doneFn();
        },
        leave: function (element, doneFn) {
            keepScroll(element, true);
            doneFn();
        }
    };
}])

Just assign the css-class .keep-scroll to your repeated elements, like this:

<div ng-repeat="item in items" class="keep-scroll">...</div>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!