implementing a lazy load and infinite scroll in AngularJS
I am planning to refactor this into a more simple, modular directive, but before doing so, I just want to describe how I approached implementing a lazy load and infinite scroll on a project I picked up. It's a dashboard for tracking individual and group progress. (Sidenote: I also would like to modularize it into an easy, near-instant dashboard library. The version I'm currently building interfaces with 2 external APIs (Github and Keen.io), as well as an API that I implemented in a nodeJS server.)
This implemenation is dependent on an API that responds once with the entire data set that will need to be fetched. Originally, I designed the api to respond to multiple calls from the client because I thought that returning too much would negetivley impact browser performance. But, that was naive. The browser can handle this without breaking a sweat.
///////////////////////
// listController.js //
///////////////////////
angular.module('myApp').controller('listController', ['$scope','dataService','$timeout', function($scope, dataService, $timeout){
$scope.data = {};
$scope.spinner = {};
$scope.data.mongo = [];
// used to show and hide the table element that will display the data
$scope.isLoading = true;
// used to manipulate the element containing the spinner once the data table
// is displayed
$scope.spinner.hide = false;
// this will store the bulk of the data from the initial response.
// then we will splice items off as needed
var cache = { data: { mongo: [] } };
var getData = function(){
// check that the view-bound data set is empty
if ($scope.data.mongo.length < 1){
dataService.getData()
.then(function(res){
// display the table element
$scope.isLoading = false;
// declare the $timeout with a reference in order to cancel
// it later. the $timeout invokes the AngularJS digest cycle
// and will update the view bound to the data
var appendDataTimer = $timeout(function(){
cache.data.mongo = res.data.mongo;
// splice some data to append to the view
$scope.data.mongo = $scope.data.mongo.concat(cache.data.mongo.splice(0, 12));
// assign some other data hashes from the response to the $scope
// so they can be referenced in the view
$scope.data.gh = res.data.gh;
$scope.data.keen = res.data.keen;
// remove the spinner from the view
$scope.spinner.hide = true;
},150);
// invoke the timeout and remove it when the promise resolves
appendDataTimer.then(function(){
$timeout.cancel(appendDataTimer);
})
});
} else {
// the view contained data, so continue to splice from the cache and add // to the view data
$scope.data.mongo = $scope.data.mongo.concat(cache.data.mongo.splice(0, 12));
// delays feel more 'magical'
$timeout(function(){
$scope.spinner.hide = true;
},1000);
}
};
// to be invoked on the scroll event in our directive, which will be
// triggered once we scroll through some data that is already displayed
$scope.loadMore = function(){
$scope.spinner.hide = false;
var loadTimer = $timeout(function(){
getData();
}, 2000)
loadTimer.then(function(){
$timeout.cancel(loadTimer);
});
};
// invoke the retrieval of data
getData();
}])
/////////////////////////////////////////
// _list_partial.html (without styles) //
/////////////////////////////////////////
<table class="table table-striped table-hover" ng-if="!isLoading" when-scrolled="loadMore()">
<thead>
<tr>
<td>NAME</td>
<td>STATS</td>
<td>COMMENTS</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="datum in data.mongo">
<td>
<span>{{ datum.First }} {{ datum.Last }}</span><br>
</td>
<td class="cohort-stats-col cohort-score"> some stats </td>
<td ><textarea rows="3">{{ datum.comments }}</textarea></td>
</tr>
</tbody>
</table>
<div ng-hide="spinner.hide">
<img src="img/spinner.gif">
</div>
//////////////////////////
// lazyLoadDirective.js //
//////////////////////////
angular.module('myApp')
.directive('whenScrolled', function($window, $timeout) {
return {
restrict: "A",
link: function(scope, element, attr) {
var top = angular.element($window)[0].screenTop;
var origHeight = angular.element($window)[0].screen.height;
var height = (origHeight * 0.9);
// bind the digest cycle to be triggered by the scroll event
// when it exceeds a threshold
angular.element($window).bind('scroll', function() {
if (angular.element($window)[0].scrollY >= (height)) {
// show the spinner when triggered
scope.spinner.hide = !scope.spinner.hide;
angular.element($window)[0].requestAnimationFrame(function(){
// invoke the function passed into the 'whenScrolled' attribute
scope.$apply(attr.whenScrolled);
// increment the threshold
height += (origHeight * 1.5);
})
}
});
}
}
})