Code Review Stack Exchange is a question and answer site for peer programmer code reviews. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

I wrote this small pagination directive after having to do something similar for an app I'm writing. For the app I wanted something that would be flexible and could be reused just about anywhere. Because of time constraints I ended up writing a directive that was very specific to that app, but now I've come back to write this to try and accomplish what I had originally hoped to do.

I'm hoping if some out there can give suggestions on:

  • Obvious bugs/flaws
  • Code style and readability
  • Hackiness (or lack of if you're generous)
  • Missing features that you would want to see in a directive like this

It's very small and fairly straightforward. Any responses are appreciated!

angular.module('paginate', [])

  /**
  * Angular Pagination Directive
  */
  .directive('pagination',         [pagination])
  .directive('column',             [column])
  .filter   ('pages',              [pages])
  .filter   ('list',               [list]);

  /**
  * Pagination Directive Function
  */
  function pagination()
  {
    var scope =                                                                               // Isolate scope for ddo
    {
      list:                        '=',                                                       // Array containing list of entries to paginate
      filters:                     '=',                                                       // Additional filters to apply
      itemLimit:                   '=',                                                       // Limit number of entries per page
      pageLimit:                   '=',                                                       // Limit number of page numbers to display
      pageNumberLocations:         '=',                                                       // Object with top and bottom properties as boolean values
      template:                    '=',                                                       // Can be framework template (e.g. bootstrap)
      classes:                     '='                                                        // Object containing classes to pass to elements on page
    };

    var ddo =                                                                                 // Directive Definition Object
    {
      restrict:                    'E',                                                       // Only available as HTML5 element                      
      scope:                       scope,                                                     // Isolate scope object
      templateUrl:                 'paginate/directive/partial/paginate.html',                // Partial location
      // template:                 template,                                                  // Page template
      link:                        link,                                                      // Link function
      transclude:                  true                                                       // Enable transclusion
    };

    /**
    * Link function for pagination directive
    * @param $scope Scope object for controller
    */
    function link($scope, el, attrs) 
    {
      // Check for list, if not throw error
      if (!$scope.list)
        throw 'You must provide an array to repeat!';

      $scope.current             = 1;                                                         // Current page number              
      $scope.position            = 0;                                                         // Current position in the repeated array
      $scope.itemLimit           = $scope.itemLimit || 10;                                    // Number of entries to limit, default 10
      $scope.pageLimit           = $scope.pageLimit || 5;                                     // Number of page numbers to limit, default 5
      $scope.pages               = Math.ceil($scope.list.length / $scope.itemLimit);          // Total number of pages for list      
      $scope.columns             = [];                                                        // Array of objects with matching heading text and properties
      $scope.pageNumberLocations = $scope.pageNumberLocations || {top: true, bottom: true}    // Object controlling display of page numbers, default to top & bottom

      // Check if 

      /**
      * Compages the page number with the current page and applies the highlight class if they match
      */
      $scope.highlight = function(page)
      {
        if ((page === $scope.current) && !!$scope.classes.highlight)
          return $scope.classes.highlight;
      };

      /**
      * Goes to the next page
      */
      $scope.next = function()
      {
        // Current cannot be more than total pages
        $scope.current           = ($scope.current < $scope.pages) ? $scope.current+1 : $scope.current;
        // Position cannot be outside of array
        $scope.position          = (($scope.list.length - $scope.position) > $scope.itemLimit) ? $scope.position + $scope.itemLimit : $scope.position;
      };

      /**
      * Goes to the previous page
      */
      $scope.prev = function()
      {
        // Current cannot be lower than 1
        $scope.current           = ($scope.current > 1) ? $scope.current-1 : $scope.current;
        // Position cannot be outside array
        $scope.position          = (($scope.position - $scope.itemLimit) >= 0) ? $scope.position - $scope.itemLimit : $scope.position;
      };

      /**
      * Goes to a specific page
      * @param page Number corresponding to the page to view
      */
      $scope.page = function(page)
      {
        if (page !== '...') {
          $scope.current         = page;                                                      // Set the page number
          $scope.position        = $scope.itemLimit * (page-1);                               // Set the position
        }
      };
    }

    return ddo;
  }

  /**
  * Column Directive Function
  */
  function column()
  {
    var scope =                                                                               // Isolate scope object
    {
      columnHeading:               '@',                                                       // Column heading text
      itemProperty:                '@'                                                        // Item property to match to column heading
    }

    var ddo =                                                                                 // Directive Definition Object
    {
      restrict:                    'E',                                                       // Allow only as element
      scope:                       scope,                                                     // Isolate scope
      link:                        link                                                       // Link function
    }

    /**
    * Directive Link Function
    * @param $scope Object providing two way binding to view
    */
    function link($scope)
    {
      var _columns;
      $scope.$parent.$parent.$watch('columns', function()                                     // Watch parent scope to add columns array
      { 
        _columns = $scope.$parent.$parent.columns;
        if (!_columns.some(function(current){ return current.property === $scope.itemProperty })) {
          _columns.push({heading: $scope.columnHeading, property: $scope.itemProperty});
        }
      });
    }

    return ddo;
  }

  /**
  * List Filter Function
  */
  function list() 
  {
    return function(repeated, position, itemLimit)                                            // Return filter function
    {
      return repeated.slice(position, (position+itemLimit));                                  // Return the subarray for current page
    }
  }

  /**
  * Pages Filter Function
  */
  function pages()
  {
    /**
    * Filter Function
    * @param repeated Array repeated
    * @param currentPage Current page being displayed
    * @param currentPosition Current position in the array
    * @param itemLimit Max number of entries per page
    * @param pageLimit Max number of page numbers to show
    */
    return function(repeated, currentPosition, currentPage, itemLimit, pageLimit)
    {
      var numPages               = Math.ceil(repeated.length / itemLimit);                    // Calculate number of pages
      var pages                  = [];                                                        // Add number to array for each page

      // Create array with all page numbers
      for (var i = 1; i <= numPages; i++) {
        pages.push(i);
      }

      /**
      * Create page numbers for an even pageLimit
      */
      function even()
      {
        var half                 = pageLimit / 2;                                             // Calculate half list size 
        pages                    = pages.slice((currentPage-(half)), (currentPage+(half)));   // Get surrounding pages
        elipses(pages);                                                                       // Add elipses and final page number to list
      }

      /**
      * Create page numbers fo an odd pageLimit
      */
      function odd()
      {
        var halfish              = Math.floor(pageLimit / 2);                                 // Calculate halfish list size
        pages                    = pages.slice((currentPage)-halfish, currentPage+(halfish)); // Get surrounding pages
        elipses(pages);                                                                       // Add elipses and final page number to list
      }

      /**
      * Create page numbers at the beginning of the list
      */
      function begin()
      {
        pages                    = pages.slice(0, pageLimit);                                 // Get subarray from begging to pageLimit
        elipses(pages);                                                                       // Add elipses and final page number to end
      }

      /**
      * Create page numbers at the end of the list
      */
      function end()
      {
        pages                    = pages.slice(((pages.length)-pageLimit), pages.length);     // Get the last n pages to display
      }

      /**
      * Add elipses to end of list
      */
      function elipses()
      {
        // Add in elipses and final page number at end of array
        pages.splice(pages.length-2, 2, '...', numPages);
      }

      // If pageLimit is greater than or equal to actual pages, just display all
      if (pageLimit >= pages.length)
        return pages;

      // If at end of list, display the final page numbers
      if ((pages.length - currentPage) < Math.floor(pageLimit / 2)) {
        end();
        return pages;
      }

      // If at the beginning, display the beginning page numbers
      if (currentPage <= Math.floor(pageLimit / 2)) {
        begin();
        return pages;
      }

      // If in the middle and even page count, make list for event amount
      if (pages.length % 2 === 0) {
        even();
        return pages;
      }

      // If in the middle and off page count, make list for odd amount
      if (pages.length % 2 !== 0) {
        odd();
        return pages;
      }
    }
  }

And the template:

<!-- 
  Example classes object:
  classes = 
  {
    ul:        'ul-class',
    prev:      'prev-class',
    page:      'page-class',
    highlight: 'highlight-class',
    next:      'next-class',
    table:     'table-class',
    thead:     'thead-class',
    theadrow:  'theadrow-class',
    th:        'th-class',
    tbody:     'tbody-class',
    tbodyrow:  'tbodyrow-class',
    td:        'td-class'
  };
-->

<!-- Page Numbering -->
<ul ng-class="classes.ul" ng-if="pageNumberLocations.top">
  <li ng-click="prev()" ng-class="classes.prev">Prev</li>
  <li ng-repeat="item in list | pages:position:current:itemLimit:pageLimit" ng-click="page(item)" ng-class="[classes.page, highlight(item)]" ng-bind="item"></li>
  <li ng-click="next()" ng-class="classes.next">Next</li>
</ul>

<!-- Column Directive -->
<ng-transclude></ng-transclude>

<!-- Paginated Table -->
<table ng-class="classes.table">
  <thead ng-class="classes.thead">
    <tr ng-class="classes.theadrow">
      <th ng-repeat="column in columns" ng-bind="column.heading" ng-class="classes.th"></th>
    </tr>
  </thead>
  <tbody ng-class="classes.tbody">
    <tr ng-repeat="item in list | list:position:itemLimit" ng-class="classes.tbodyrow">
      <td ng-repeat="column in columns" ng-bind="item[column.property]" ng-class="classes.td"></td>
    </tr>
  </tbody>
</table>

<!-- Page Numbering -->
<ul ng-class="classes.ul" ng-if="pageNumberLocations.bottom">
  <li ng-click="prev()" ng-class="classes.prev">Prev</li>
  <li ng-repeat="item in list | pages:position:current:itemLimit:pageLimit" ng-click="page(item)" ng-class="[classes.page, highlight(item)]" ng-bind="item"></li>
  <li ng-click="next()" ng-class="classes.next">Next</li>
</ul>

Which leaves us with an implementation something like:

<pagination list="list" item-limit="9" page-limit="8" classes="classes">
  <column item-property="data1" column-heading="This"></column>
  <column item-property="data2" column-heading="Is"></column>
  <column item-property="data3" column-heading="Awesome!"></column>
</pagination>

Again, any suggestions or feedback is greatly appreciated!

share|improve this question

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Browse other questions tagged or ask your own question.