New version Angular 9
JQueryLess Grid and Pagination
-
Next
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!

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,
- If you have Python 2.x: python -m SimpleHTTPServer 8000
- If you have Python 3.x: python -m http.server 8000
- If you have Perl:
- cpan HTTP::Server::Brick # install dependency
- perl -MHTTP::Server::Brick -e '$s=HTTP::Server::Brick->new(port=>8000); $s->mount("/"=>{path=>"."}); $s->start'
- If you are under windows: "C:\Program Files (x86)\IIS Express\iisexpress.exe" /path:%CD% /port:8000
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:
- If the current page is the 1st page don't show Back
- If the 1st page button is visible don't show First button
- If the last page is visible don't show Last button
- If the current page is the last page don't show Next button
- Show at most "maxPages" pages at the same time
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
- restrict: This tells how the directive can be applied (the types can be mixed!)
- A: Attribute, the directive is an attribute (sg-grid)
- E: Element, the directive is a real html element (sg-grid, like div)
- C: CSS class, the directive will be applied on items with the class specified (class='sg-grid')
- require: This tells that the directive will need an item with the given directive on the same element.
We need an ngModel (that will be the data managed by the grid - scope: The scope for the directive. Can be:
- false: Use the scope of the controller enclosing the directive
- true: Create a new scope, child of the controller scope
- {...}: Create a new "isolated" scope (disconnected) with the given parameters.
- link: The function executed to bind the directive to the scope (executed every time the directive is
instantiated
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.
- xName:'=': The content of the attribute x-name will be passed into the scope. Modification made on the
variable by the enclosing controller will be reflected on scope.xName and vice-versa.- myName:'=xName': The attribute x-name will be transalated into the variable scope.myName
- xName:'=?': The attribute is optional
- myName:'=?xName': The attribute is optional
- xName:'@': The scope.xName will contain the string in the attribute x-name. If the attribute is of the
form x-name="{{myFunc()}}" the attribute will contain the result of myFunc- The optional and naming conventions are the same as for '='
- xName:'&': The parameter passed is a function.
- If the function is specified as x-name="myFunc(a,b,...)" it can be invoked as scope.xName(). In fact this
will be as calling a function of the form function(){myFunc(a,b,...);} - If the function is specified as x-name="myFunc" it can be invoked as scope.xName()(). If the function has
parameters can be invoked as scope.xName().(a,b,...)
- If the function is specified as x-name="myFunc(a,b,...)" it can be invoked as scope.xName(). In fact this
Back on the directive
We need several values
- sgCurrentPage: The page currently selected
- sgPageSize: The size of a page
- sgMaxPages: The maximum number of visible pages
- sgCount: The total count of data
- sgButtons: The variable containing the buttons to show
- sgLoadData: A function ( function(selectedPage) ) to asssociate to buttons to change page
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
- $watch('variableName',function(new,old)): When the scope.variableName changes the function is invoked
- $watch(function(),function()): When the value given by the first function is changed then call the second function
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>