New version Angular 9
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!
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,
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 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
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.
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; });
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]); }
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); }); }
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>