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>

Last modified on: April 21, 2015