New version Angular 9

JQueryLess Grid and Pagination

In this part we will discover how to create a grid directive with a pagination service.
I did not wanted to use the standard ngGrid because of its inclusion of JQuery.
For the filtering go on the next page!

Note: I don't hate JQuery, it's awesome, but when in Angular do as Angular do!

Screenshot

Download

The source for this part is here: 08_pagination.zip.

The library created based on this tutorial is on the sgGrid

The sample can be run with Firefox, in general or by running, into the directory,

The grid directive

The Pagination Service

First we will need to create a service that given the data retrieved by the server will return a list of buttons
to show for the pagination. First the specs:

We define the data that will be passed to the pagination service

	var paginationDescriptor = {
		currentPage,
		maxPages,	//The maximum number of pages visible
		count,		//The number of total available items
		pageSize,	//The number of item per pages
	}

The button descriptor will be

	var buttonDescriptor = {
		type,		//<< (first), < (previous), > (next), >> (last), # (number)
		index,		//The page index (0 based)
		selected	//If is the current page
	}

And the service will be

common.service('sgGridPaginationService', [function() {
	return function(pd){
		var buttons = [];
		var lastPage = 0;
		var startAt =0;
		var totalPages = -1;
		
		var createButton = function(type,index,selected){
			buttons.push({
				type:type,
				index:index,
				selected:selected?selected:false
			});
		}

		var halfMaxPages = pd.maxPages/2;
		startAt = 0;
		if(pd.currentPage > halfMaxPages){
			startAt = pd.currentPage - halfMaxPages;
		}
		totalPages = Math.ceil(pd.count/pd.pageSize);
		lastPage = Math.min(startAt+pd.maxPages,totalPages);
			
		if(startAt>0){
			createButton('<<',0);
		}
		
		if(pd.currentPage>0){
			createButton('<',pd.currentPage-1);
		}
		
		for(var i=startAt;i<lastPage;i++){
			createButton('#',i,i==pd.currentPage);
		}
		
		if(pd.currentPage<(totalPages-1)){
			createButton('>',pd.currentPage+1);
			if(pd.currentPage<(totalPages-pd.maxPages/2)){
				createButton('>>',totalPages-1);
			}
		}
		
		return buttons;
	}
}]);

The directive

The stub we will use for the directive is

common.directive("sgGrid",['$http','sgGridPaginationService',
	function($http,paginationService){
	return {
		restrict:'A',
		require: 'ngModel',
		scope:{
			sgCurrentPage:'=',
			sgPageSize:'=',
			sgMaxPages:'=',
			sgCount:"=",
			sgButtons:"=",
			sgLoadData:'&'
		},
		link: function(scope, element, attrs,ngModel){}
	}}]);

The parameters are

The isolated scope

Inside the isolated scope we declare the attributes that will be "copied" on the directive. These
are defined as name:'specification' where name is the variable that will be added on the directive scope
when linking. In our situation into the link scope i'll found the scope.sgCurrentPage variable.

Back on the directive

We need several values

All the variables must be defined on the controller enclosing the directive.

The directive, essentially will check the data contained into the ngModel and will reload the
buttons accordingly. The same will happen when changing the page size.

We will watch the page size, and if it changes the data will be reloaded.

The scope.$watch takes 2 parameters, with several overloads

In case we use collections exists the $watchCollection function that has the same signatures.

scope.$watch(function(){
				return scope.sgPageSize;
			},function(){
				scope.sgLoadData()(0)
			})	

Then we will watch the data. When the data changes, the buttons will be changed. Note the special
value "ngModel.$modelValue". This is a trick to get the model value in directives!

			//Look for data changes
			scope.$watchCollection(function () {
				return ngModel.$modelValue;
			}, function() {
				var currentPage = scope.sgCurrentPage;
				
				//Find the first page to show
				var startPage = currentPage>0?Math.min(currentPage,currentPage-scope.sgMaxPages):0;
				
				//Prepare the pagination
				var paginationDescriptor = {
					currentPage:scope.sgCurrentPage,
					maxPages:scope.sgMaxPages,
					count:scope.sgCount,
					pageSize:scope.sgPageSize
				}
				
				var result = paginationService(paginationDescriptor);
				
				var newButtons=[];
				//Create the buttons
				for(var i=0;i<result.length;i++){
					var r = result[i];
					newButtons.push({
						label:r.type=="#"?r.index+1:r.type,
						pageIndex:r.index,
						selected:r.selected,
						//Here we use the this.pageIndex. If we use i, we will
						//use its reference!!!
						go:function(){scope.sgLoadData()(this.pageIndex);}
					});
				}
				//Set the buttons
				scope.sgButtons = newButtons;
		   });

The data service

We will change the data service to handle paging requests. The function "list" will be changed. Note
that we added the "count" parameter too, to define how many items we will need.

	this.list=function(currentPage,pageSize,count){
		var start = currentPage * pageSize;
		var end = start + count;
		var result = "/customers?range=["+start+","+end+"]";
		return result;
	};

We will add even a function to get the total data length from headers (fakerest stores there this value)

	this.getListCount = function(data,headers){
		var contentRange = headers()['content-range'];
		var length = contentRange.split('/');
		return parseInt(length[1]);
	}

The controller

Into the generic list controller we will set the new variables that will be used by the grid

		$scope.pageSize = 10;
		$scope.maxPages = 10;
		$scope.totalCount = 0;
		$scope.currentPage = 0;

And we will add the paging to the loadData. Note that we require 1 item more than the page size.
This because if we have not the total count available we can still know if there is a "next".

Of course when we set the data, we remove the (eventual) last row of data!!


		$scope.loadData = function(requiredPage){
			//Sanity check
			if(!requiredPage){
				requiredPage = 0;
			}
			
			//Getting the address
			var address= dataService.list(requiredPage,$scope.pageSize,$scope.pageSize+1);
			$http.get(address)
				.success(function(data, status, headers, config){
					var listTotal = 0;
					
					$scope.hasNext = data.length > $scope.pageSize;
					if($scope.hasNext){
						data = data.splice(0,$scope.pageSize);
					}
					//If has a count
					if(dataService.getListCount){
						listTotal = dataService.getListCount(data,headers);
					}else{
						listTotal = $scope.pageSize*(requiredPage+1) + ($scope.hasNext?1:0);
					}
					
					$scope.currentPage = requiredPage;
					
					$scope.listTotal = listTotal;
					if(callbacks.postLoadData)data = callbacks.postLoadData(data,headers);
					$scope.data = data;
				})
				.error(function(data,status,headers,config){
					globalMessagesService.showMessage(data.message,status);
				});
		}

The template

We wrap all the grid data into a div with the sg-grid attribute. We add too a navigation block containing
the buttons and invoking the "go()" function on the buttons. We set all the sg-* attributes on the sg-grid.

<div sg-grid
			ng-model="data"
			sg-page-size="pageSize" 
			sg-load-data="loadData" 
			sg-max-pages="maxPages"
			sg-current-page="currentPage"
			sg-count="listTotal"
			sg-buttons="buttons">
		<nav>
			<ul class="pagination">
				<li ng-repeat="button in buttons" ng-class="{'active':button.selected}">
						<a ng-click="button.go()" >{{button.label}}</a>
				</li>
			</ul>
		</nav>
		<!-- HERE GOES THE OLD GRID -->
	</div>