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!