DEMO
List of objects is rendered using ng-repeat
. Suppose that this list is very long and user is scrolling to the bottom to see some objects.
W
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.
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 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 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>
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).