AngularJS - Sort, Filter and Paging - A Table Directive

AngularJS is GREAT!
you should neglect whatever framework you are using right now,
and switch to AngularJS immediately!

In the following posts I will show you how you can easily
write a reusable table with sorting, filter and paging!
And it is all reusable, customizable and easy - thanks to AngularJS! You guys rock!
At the end I will show you how it all comes together,
and I will provide a github link to the source code.
This example is great for advanced programming in AngularJS.
For production purposes you should definitely check out AngularUI's NG-Grid project.
Outcome
Weeks after writing these posts (not all published yet) - I came across a nice project called "smart table" - what do you know?
After you read my posts, you will know how to write this "smart table" plugin in AngularJS.
Let me use this opportunity to point out this plugin did a nice job with the filtering feature.
It simply placed an input field on every column header - which is very intuitive and space is used well.
More Possible Features

What I show in these posts is the basics.
You can easily modify them to include server side filtering,
customizable templates, page jump links and probably a lot more.
Getting Started
Before we begin implementing the features, we need to set the environment.
For data, I decided to use a nifty cool tool for JSON data generation. However, since they will not save my JSON for longer than 30 days,
I saved their output into a google doc, and I serve it from my google drive.

Below you can find an HTML template to start from.
This template shows a simple AngularJS usage for building a table from JSON data.
I added some basic CSS to comfort.
For some reason, using $http.get did not go that well, so I used JQuery instead.
<html ng-app="myApp">
 <head>
  <style>
   body {background-color:#cecece; margin:0; padding:0; font-family:Arial; }
   body>div:first-child {width:50%; margin:auto; margin-top:40px;}
   table {border:none; border-collapse:collapse; width:100%;}
   table tr { border:none;}
   table tr td { border:none; font-size:37px; font-weight:bolder;}
   table tr:first-child{background-color:#00ff00;}
   table tr:nth-child(2n){ background-color:blue;}
   table tr:nth-child(2n+3){ background-color:red;}
  </style>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
  <script src="http://code.angularjs.org/1.0.6/angular.min.js"></script>

  <script type="text/javascript">
 
   var myApp = angular.module("myApp",[]);

   myApp.controller("PostController", function( $scope, $http ){
    $scope.headers = ["name","age"];
    $scope.availableHeaders = [];
    // using JQuery because $http does not work as expected here..
    $.ajax({
     url:"https://googledrive.com/host/0BzBTj4P1uKcAMVVNa0VySm5fbjg",
     success:function(d){
      $scope.$apply(function(){
       $scope.data = JSON.parse(d)["result"];
       for ( header in $scope.data ){
        $scope.availableHeaders.push(header);
       }
       console.log($scope.data)
      });    
    }
    });
   });
  </script>
 </head>
 <body ng-controller="PostController">
  <div>
   <table>
    <tr>
     <td ng-repeat="head in headers">
      {{head}}
     </td>
       </tr>
       <tr ng-repeat="d in data">
        <td ng-repeat="head in headers">
         {{d[head]}}
        </td>
       </tr>
   </table>
  </div>
 </body>
</html>
The complete code
Here is the code as it should look like at the end.

<html ng-app="myApp">
  <head>
<style>
body {background-color:#cecece; margin:0; padding:0; font-family:Arial; }
body>div:first-child {width:50%; margin:auto; margin-top:40px;}
table {border:none; border-collapse:collapse; width:100%;}
table tr { border:none;}
table tr td { border:none; font-size:37px; font-weight:bolder;}
table tr:first-child{background-color:#00ff00;}
table tr:nth-child(2n){ background-color:blue;}
table tr:nth-child(2n+3){ background-color:red;}
table tr:first-child td.sortBy:after{
display:block;
content:"";
height:0;
width:0;
border:10px solid transparent;
}
table tr:first-child td.desc:after{
border-color: black transparent transparent transparent;
}

table tr:first-child td.asc:after{
border-color: transparent transparent black transparent;
}

</style>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script src="http://code.angularjs.org/1.0.6/angular.min.js"></script>

<script type="text/javascript">

var myApp = angular.module("myApp",[]);

myApp.controller("PostController", function( $scope, $http ){
$scope.headers = ["name","age"];
$scope.dataPageSize = 30;
$scope.setPageSize = function(pageSize){$scope.dataPageSize = pageSize;}
$scope.toggleHeader = function( header ){
var headerIndex = $scope.headers.indexOf(header);
if (  headerIndex >= 0 ){
$scope.headers.splice(headerIndex,1);
}else{
$scope.headers.push(header);
}
};

$scope.orderTableBy = function(header){
                    if ( $scope.orderHeader == header && $scope.orderDirection == false){
                        $scope.orderHeader = null; // clear sort.
                    }
                    else if ( $scope.orderHeader == header ){
                        $scope.orderDirection = false;
                    }else{
                      $scope.orderHeader = header;
                        $scope.orderDirection = true;
                    }
                };

$scope.availableHeaders = [];
// using JQuery because $http does not work as expected here..
$.ajax({
url:"https://googledrive.com/host/0BzBTj4P1uKcAMVVNa0VySm5fbjg",
success:function(d){
$scope.$apply(function(){
$scope.data = JSON.parse(d)["result"];
for ( header in $scope.data[0] ){
$scope.availableHeaders.push(header);
}
console.log($scope.data)
});
}
});
});

myApp.filter("pagingFilter", function(){
return function(input, currentPage, pageSize ){
return input ?  input.slice(currentPage * pageSize, currentPage * ( pageSize + 1 )) : [];
}

});
myApp.directive("paging", function(){

        return {
            template:'<div><button ng-disabled="!hasPrevious()" ng-click="onPrev()"> Previous </button> {{start()}} - {{end()}} out of {{size()}} <button ng-disabled="!hasNext()" ng-click="onNext()"> Next </button><div ng-transclude=""></div> </div>',
            restrict:'AEC',
            transclude:true,
            scope:{
                'currentPage':'=',
                'pageSize':'=',
                'data':'&'

            },
            link:function($scope, element, attrs){

                $scope.size = function(){

                    return angular.isDefined($scope.data()) ? $scope.data().length : 0;
                };

                $scope.end = function(){
                    return $scope.start() + $scope.pageSize;
                };

                $scope.start = function(){
                    return $scope.currentPage * $scope.pageSize;
                };

                $scope.page = function(){
                    return !!$scope.size() ? ( $scope.currentPage + 1 ) : 0;
                };

                $scope.hasNext = function(){
                    return $scope.page() < ( $scope.size() /  $scope.pageSize )  ;
                };

                $scope.onNext = function(){
                    $scope.currentPage = parseInt($scope.currentPage) + 1;
                };

                $scope.hasPrevious = function(){
                    return !!$scope.currentPage;
                } ;

                $scope.onPrev = function(){
                    $scope.currentPage=$scope.currentPage-1;
                };

                try{
                    if ( typeof($scope.data) == "undefined"){
                        $scope.data = [];
                    }
                    if ( typeof($scope.currentPage) == "undefined" ){
                        $scope.currentPage = 0;
                    }
                    if ( typeof($scope.pageSize) == "undefined"){
                        $scope.pageSize = 10;
                    }
                }catch(e){ console.log(e);}
            }

        }

})


</script>
</head>
<body ng-controller="PostController">
<div>
<div class="search-bar">
        <label>Search</label><input ng-model="searchText">
    </div>
<div class="available-headers">
<span class="available-header" ng-click="toggleHeader(header)" ng-repeat="header in availableHeaders" style="border:1px solid black; padding:10px; border-radius:10px; line-height:40px;">
{{header}}
</span>
</div>
<div class="page-size" style="padding-top:10px; padding-bottom:10px;">
page size :
<a href="javascript:void(0)" style="padding-left:10px" ng-click="setPageSize(pageSize)" ng-repeat="pageSize in [10,20,30]"> {{pageSize}}</a>
</div>
<paging data="tableData = ( data | orderBy:orderHeader:orderDirection | filter:searchText  )" current-page="dataCurrentPage" page-size="dataPageSize">
<table>
<tr>
<td
ng-class="{
'sortBy' : head == orderHeader,
'asc':head == orderHeader && orderDirection == true,
'desc':head == orderHeader && orderDirection == false
}"
ng-click="orderTableBy(head)"
ng-repeat="head in headers">
{{head}}
</td>
    </tr>
    <tr ng-repeat="d in tableData  | pagingFilter:dataPageSize:dataCurrentPage">
    <td ng-repeat="head in headers">
    {{d[head]}}
    </td>
    </tr>
</table>
Found {{tableData.length}} search results
  </paging>
</div>
</body>
</html>

If you enjoyed this article, you might also like..