Paginating Through Client-Side Data

Problem

You have a table of data completely client-side and want to paginate through the data.

Solution

Use an HTML table element with the ng-repeat directive to render only the items for the current page. All the pagination logic should be handled in a custom filter and controller implementation.

Let us start with the template using Twitter Bootstrap for the table and pagination elements:

<div ng-controller="PaginationCtrl">
  <table class="table table-striped">
    <thead>
      <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Description</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="item in items |
        offset: currentPage*itemsPerPage |
        limitTo: itemsPerPage">
        <td>{{item.id}}</td>
        <td>{{item.name}}</td>
        <td>{{item.description}}</td>
      </tr>
    </tbody>
    <tfoot>
      <td colspan="3">
        <div class="pagination">
          <ul>
            <li ng-class="prevPageDisabled()">
              <a href ng-click="prevPage()">« Prev</a>
            </li>
            <li ng-repeat="n in range()"
              ng-class="{active: n == currentPage}" ng-click="setPage(n)">
              <a href="#">{{n+1}}</a>
            </li>
            <li ng-class="nextPageDisabled()">
              <a href ng-click="nextPage()">Next »</a>
            </li>
          </ul>
        </div>
      </td>
    </tfoot>
  </table>
</div>

The offset Filter is responsible for selecting the subset of items for the current page. It uses the slice function on the Array given the start param as the index.

app.filter('offset', function() {
  return function(input, start) {
    start = parseInt(start, 10);
    return input.slice(start);
  };
});

The controller manages the actual $scope.items array and handles the logic for enabling/disabling the pagination buttons.

app.controller("PaginationCtrl", function($scope) {

  $scope.itemsPerPage = 5;
  $scope.currentPage = 0;
  $scope.items = [];

  for (var i=0; i<50; i++) {
    $scope.items.push({
      id: i, name: "name "+ i, description: "description " + i
    });
  }

  $scope.prevPage = function() {
    if ($scope.currentPage > 0) {
      $scope.currentPage--;
    }
  };

  $scope.prevPageDisabled = function() {
    return $scope.currentPage === 0 ? "disabled" : "";
  };

  $scope.pageCount = function() {
    return Math.ceil($scope.items.length/$scope.itemsPerPage)-1;
  };

  $scope.nextPage = function() {
    if ($scope.currentPage < $scope.pageCount()) {
      $scope.currentPage++;
    }
  };

  $scope.nextPageDisabled = function() {
    return $scope.currentPage === $scope.pageCount() ? "disabled" : "";
  };

});

You can find the complete example on github.

Discussion

The initial idea of this pagination solution can be best explained by looking into the usage of ng-repeat to render the table rows for each item:

<tr ng-repeat="item in items |
  offset: currentPage*itemsPerPage |
  limitTo: itemsPerPage">
  <td>{{item.id}}</td>
  <td>{{item.name}}</td>
  <td>{{item.description}}</td>
</tr>

The offset filter uses the currentPage*itemsPerPage to calculate the offset for the array slice operation. This will generate an array from the offset to the end of the array. Then we use the built-in limitTo filter to subset the array to the number of itemsPerPage. All this is done on the client side with filters only.

The controller is responsible for supporting a nextPage and prevPage action and the accompanying functions to check the disabled state of these actions via ng-class directive: nextPageDisabled and prevPageDisabled. The prevPage function first checks if it has not reached the first page yet before decrementing the currentPage and the nextPage does the same for the last page and the same logic is applied for the disabled checks.

This example is already quite involved and I intentionally omitted an explanation of the rendering of links between the previous and next buttons. The full implementation is online though for you to investigate.