How to retain the scroll position of ng-repeat in AngularJS when removing an item from the top

Problem

I tried to work from the solution to this

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

to achieve retaining the scroll position when removing the top item in an ng-repeat but couldn't figure out the code to do so.

Also, side note, the list should print in the same order as the items array, not in the reverse as the example does.

The solution's code:

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.addNewItem = 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.log("Adding item", item, item.clientHeight);
        element.scrollTop = (element.scrollTop+item.clientHeight+1); //1px for margin
      };

    },

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

What I tried doing was changing

element.scrollTop = (element.scrollTop + item.clientHeight+1)

to

element.scrollTop = (element.scrollTop - item.clientHeight+1)

and printing in order by 'id' not '-id'

Problem courtesy of: mohamed.ahmed

Solution

I think the initial solution is kind of hacky... but here's a working edit using it as the basis.

The problem is that the solution depends on items being added to the ng-repeat. If you look at the scrollItem directive, it only causes the keepScroll directive to readjust scrollTop if the linker gets executed. This only happens when items get added, not removed.

The edit instead listens to the scope.$on('$destroy') event. The issue at that point is however, that the element no longer has a clientHeight because it has been removed from the DOM. So the solution is to save its height when it gets created, and then instead tell keepScroll what the height of the removed element was.

Note: This seemed to cause a scroll jump if the scroller was all the way to the bottom, so you'd need to look into that case as an exception.

Working JSBin: http://jsbin.com/geyapugezu/1/edit?html,css,js,output

For reference:

HTML

<!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 | orderBy: 'id'">
      {{ item.name }}
    </div>
  </div>
  <button ng-click="removeItem()">
    Remove item
  </button>
</body>
</html>

CSS

.wrapper {
  width: 200px;
  height: 300px;
  border: 1px solid black;
  overflow: auto;
}
.item {
  background-color: #ccc;
  height: 100px;
  margin-bottom: 1px;
}

JS

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.removeItem = function() {
      $scope.items = $scope.items.slice(1);
    };
})
.directive("keepScroll", function(){

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

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

      this.itemRemoved = function(height){
        element.scrollTop = (element.scrollTop - height - 1); //1px for margin
        console.log("Item removed", element.scrollTop);
      };

    },

    link : function(scope,el,attr, ctrl) {
     ctrl.setElement(el[0]);

    }

  };

})
.directive("scrollItem", function(){


  return {
    require : "^keepScroll",
    link : function(scope, el, att, scrCtrl){
      var height = el[0].clientHeight;

      scope.$on('$destroy', function() {
        scrCtrl.itemRemoved(height);
      });
    }
  };
});

EDIT

Or, do this. No need for scrollItem, instead we watch changes to the ng-repeat items and readjust the scrollTop accordingly.

JSBin: http://jsbin.com/dibeqivubi/edit?html,css,js,output

JS

angular.module("Demo", [])
  .controller("DemoCtrl", ['$scope', function($scope) {
    $scope.items = [];

    for (var i = 0; i < 10; i++) {
      $scope.items[i] = {
        id: i,
        name: 'item ' + i
      };
    }

    $scope.removeItem = function() {
      $scope.items = $scope.items.slice(1);
    };
}])
.directive("keepScroll", function() {
  return {
    link : function(scope,el,attr, ctrl) {
      var scrollHeight;

      scope.$watchCollection('items', function(n,o) {
        // Instantiate scrollHeight when the list is
        // done loading.
        scrollHeight = scrollHeight || el[0].scrollHeight;
        // Adjust scrollTop if scrollHeight has changed (items
        // have been removed)
        el[0].scrollTop = el[0].scrollTop - (scrollHeight - el[0].scrollHeight);
        // Remember current scrollHeight for next change.
        scrollHeight = el[0].scrollHeight;
      });
    }

  };
});

HTML

<!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" ng-repeat="item in items | orderBy: 'id'">
      {{ item.name }}
    </div>
  </div>
  <button ng-click="removeItem()">
    Remove item
  </button>
</body>
</html>
Solution courtesy of: foglerek

Discussion

I'm not sure if i understand correctly, but you can achieve what you want with listening the items array and item to be removed.

Hope this will help

http://plnkr.co/edit/buGcRlVGClj6toCVXFKu?p=info

Here's what i did:

Added a height property for items

for (var i = 0; i < 20; i++) {
    $scope.items[i] = {
        id: i,
        name: 'item ' + i,
        height: (Math.random()*100)+30
    };
}

style: height property inside the html file

<div class="wrapper" keep-scroll>
    <div class="item" scroll-item ng-repeat="item in items" style="height:{{item.height}}px">
        {{ item.name }}
    </div>
</div>

deleteItem method inside the DemoCtrl

$scope.deleteItem = function() {
    var itemToDelete = $scope.items[0];
    $scope.items.splice(0,1);
    $scope.$broadcast("scrollFix",itemToDelete);
};

Than i listen scrollFix event inside the keepScroll directive

$scope.$on('scrollFix',function(event,data){
   element.scrollTop = element.scrollTop - data.height;
});
Discussion courtesy of: huseyinozcan

I hope $anchorScroll can help you. Follow the link.

Discussion courtesy of: lokeshjain2008

This recipe can be found in it's original form on Stack Over Flow.