7

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'

Community
  • 1
  • 1
shadowcursor
  • 1,174
  • 1
  • 13
  • 19

3 Answers3

5

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>
foglerek
  • 178
  • 1
  • 1
  • 8
  • This is the best solution. Very complete and follows good Angular practices. As you mentioned however, with both solutions is if it's scrolled to the bottom and the remove item button is clicked, the scroll jumps up to some arbitrary position. What would the cause of that be? – shadowcursor Dec 06 '15 at 01:11
0

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

lokeshjain2008
  • 1,879
  • 1
  • 22
  • 36
  • This doesn't really do what I want it to. Not all the items would have the same height, and I wouldn't necessarily want to scroll to the top of the item. – shadowcursor Nov 15 '15 at 21:28
0

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;
});
  • Why is the height property necessary for adding an item though it was not necessary for removing it as in the linked previous solution? – shadowcursor Dec 02 '15 at 06:57
  • Also broadcasting an event on the root scope isn't good practice. I'd like to use a directive as in the original solution and as per best practices for manipulating DOM elements. – shadowcursor Dec 02 '15 at 09:32
  • Yes, you are right muhamed. It's not necessary to define height for items. Foglerek's solution is more convenient – huseyinozcan Dec 03 '15 at 06:07