Created
April 16, 2015 20:33
-
-
Save jimmont/1ad12db39dc112b14db6 to your computer and use it in GitHub Desktop.
paginate
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.directive('paginateThis', function(paginate, $parse, $location){ | |
return { | |
restrict: 'A', | |
link: function (scope, elem, attr){ | |
function mapOnToModel(res){ | |
var p = scope.paginate | |
, _esp = JSON.stringify( res.options.query ) | |
, _sort = JSON.stringify( res.options.sort ) | |
; | |
// if the request doesn't match the moving target (the Paginator instance) | |
// then the current esp query has changed since this paginate was first made | |
if ( p._esp !== _esp || p._sort !== _sort ){ | |
p.model = new Array( scope.stuff.total ); | |
// keep the current page unless the query changed | |
p.update( {current: p._esp !== _esp ? 1 : p.current} ); | |
p._esp = _esp; | |
p._sort = _sort; | |
}; | |
var i = 0 | |
, results = res.results | |
, l = results.length | |
, from = res.options.from | |
, model = p.model; | |
// note the res.options.from might get out of sync with paginate.index because the http-request is async | |
// if the first item in the response is already in the corresponding model location assume we've already mapped this set onto the model | |
if( (results[0]||{id:Infinity}).id === (p.model[ from ]||{}).id ) return; | |
while(i < l){ | |
model[ from + i ] = results[ i ]; | |
i++; | |
}; | |
scope.paginate.updateItems( 'force' ); | |
}; | |
// paginate.index tracks through the model, drives the update | |
scope.$watch('paginate.index', function(index, old, scope){ | |
$location.esp({page: scope.paginate.current}); | |
// if there's no first value assume we need a new set | |
// TODO remove the scope.initialized and encapsulate this concern in scope.refresh | |
if( scope.paginate.items[0] || !scope.initialized ) return; | |
scope.getThoseResults({from: index, size: scope.paginate.size}).then(mapOnToModel); | |
}); | |
// eg: attr.paginateThose = '{size:10}' | |
scope.paginate = $parse( attr.paginateThose )( scope ) | |
scope.paginate.current = +($location.esp().page || 1); | |
scope.paginate = paginate( scope.paginate ); | |
function relayToModel(nu, old, scope){ | |
if (!nu) return; | |
mapOnToModel(scope.stuff); | |
}; | |
scope.$watch('stuff.options.query', relayToModel); | |
scope.$watch('stuff.options.sort', relayToModel); | |
// refresh when model changes | |
scope.$watchCollection('paginate.model', function(nu, old, scope){ | |
scope.paginate.update(); | |
var current = scope.paginate.current; | |
if(scope.paginate.last && current > scope.paginate.last){ | |
current = scope.paginate.current = scope.paginate.last; | |
scope.paginate.updateItems(); | |
}else if(current < scope.paginate.first){ | |
current = scope.paginate.current = scope.paginate.first; | |
scope.paginate.updateItems(); | |
}; | |
}); | |
/* TODO | |
elem | |
.on('scrolltotop', scope.paginate.prev) | |
.on('scrolltoend', scope.paginate.next) | |
*/ | |
} | |
}; | |
}) | |
.factory('paginate', function(){ | |
/** | |
* @ngdoc service | |
* @description | |
setup the object and methods for generic pagination | |
extend the basic functionality elsewhere as-desired | |
* @param {object} [setup] object definition with options | |
* @param {boolean} [setup.loop=true] | |
* @param {number} [setup.size=1] number of items per-page | |
* @param {number} [setup.current=1] active page | |
* @param {array} [setup.model=[]] array of items to paginate | |
* @example | |
<form paginate-this="{size:10}"> | |
... | |
<tbody ng-if="paginate.items.length" ng-repeat="row in paginate.items"> | |
... | |
<div ng-if="paginate.last > 1" class="pager"><button class="prev-button" ng-disabled="paginate.current === paginate.first" data-ng-click="paginate.prev()">Prev</button><button class="number-button" data-ng-click="paginate.goTo(page)" data-ng-repeat="page in paginate.numbers" ng-class="{active: page === paginate.current}">{{page}}</button><button class="next-button" ng-disabled="paginate.current === paginate.last" ng-click="paginate.next()">Next</button></div> | |
... | |
</form> | |
* @return | |
Paginator instance { | |
model: [items to paginate], | |
items: [items for current page from model], | |
_length: model.length, | |
size: number of items per page, | |
loop: boolean, | |
index: array index in this.model for first item on the active page, | |
first: number for 1st page, | |
last: number of total pages, | |
current: number active page base-1, | |
numbers: array of page-numbers from first to last eg [1,2,3...] | |
} | |
* @method | |
all methods return the instance | |
instance.goTo( number ) take me to page given, if it exists, otherwise a sensible (possibly looping) default | |
instance.prev( ) | |
instance.next( ) | |
instance.update( ) update properties: _length, first, last, index, items | |
*/ | |
function Paginator(setup){ | |
this.loop = 'boolean' === typeof(setup.loop) ? setup.loop : true; | |
// any data source with Array-like methods and properties | |
// note the optional length where an empty array is created for later population | |
this.model = setup.model || []; | |
// starting page | |
this.current = setup.current || 1; | |
this.first = 1; | |
// items to include on a page | |
this.size = setup.size || 1; | |
this.update(); | |
}; | |
Paginator.prototype = { | |
updateItems: function(force){ | |
// find the starting point in the whole array | |
var l = (this.current - 1) * this.size; | |
force = force || l !== this.index; | |
if(force){ | |
// allow passing in the current page to jump to a specific one | |
if(force.current){ | |
// validate and set this.current | |
this.goTo( force.current ); | |
l = (this.current - 1) * this.size; | |
}; | |
// if the index changed then update items (force it for deep model changes to propagate) | |
this.index = l; | |
// [1,2,3,4,5].slice(0,2); returns [1,2] | |
this.items = this.model.slice(l, l + this.size); | |
}; | |
return this; | |
}, | |
update: function(force){ | |
// base 1, page min/max: | |
// min = 1 | |
// max = this.model.length / size (or items) per page | |
var l = this.model.length; | |
if(force || this._length !== l){ | |
this._length = l; | |
this.last = Math.ceil( l / this.size ); | |
this.numbers = []; | |
l = this.first; | |
do{ | |
this.numbers.push( l++ ); | |
}while(l <= this.last); | |
// when the model changes ensure the index and items are updated too | |
// passing current results in validation of the value | |
force = (force && force.current) ? force : {current: this.current}; | |
}; | |
return this.updateItems( force ); | |
}, | |
goTo: function page( n ){ | |
// assume n is always a number | |
if( n < this.first ) n = this.loop ? this.last : this.first; | |
else if( n > this.last ) n = this.loop ? this.first : this.last; | |
this.current = n; | |
return this.updateItems(); | |
}, | |
next: function nextPage(){ | |
return this.goTo( this.current + 1 ); | |
}, | |
prev: function previousPage(){ | |
return this.goTo( this.current - 1 ); | |
} | |
}; | |
return function(setup){ | |
return new Paginator(setup || {}); | |
}; | |
}) | |
/* unit test */ | |
describe('paginate', function(){ | |
var paginate; | |
beforeEach(inject(function(_paginate_) { | |
paginate = _paginate_; | |
})); | |
it("should create a default pagination object", function(){ | |
var instance = paginate(); | |
expect( JSON.stringify( instance.model ) ).toBe( '[]' ); | |
expect( JSON.stringify( instance.items ) ).toBe( '[]' ); | |
expect( instance.numbers.join() ).toBe( '1' ); | |
expect( instance.first ).toBe( 1 ); | |
expect( instance.last ).toBe( 0 ); | |
expect( instance.size ).toBe( 1 ); | |
expect( instance.current ).toBe( 1 ); | |
expect( instance.loop ).toBe( true ); | |
expect( instance._length ).toBe( 0 ); | |
expect( instance.index ).toBe( 0 ); | |
}); | |
it("should paginate something and work (update, goTo, next, prev)", function(){ | |
var instance = paginate({ | |
model: 'abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01234567-_'.split('') | |
,size:10 | |
}); | |
expect( instance.model.length ).toBe( 72 ); | |
expect( instance.items.length ).toBe( 10 ); | |
expect( instance.items.join('') ).toBe( 'abcdefghij' ); | |
expect( instance.numbers.length ).toBe( 8 ); | |
// this messes up the numbers, but it's just a test | |
expect( instance.numbers.pop() ).toBe( 8 ); | |
expect( instance.numbers.length ).toBe( 7 ); | |
expect( instance._length ).toBe( 72 ); | |
expect( instance.index ).toBe( 0 ); | |
// fix the numbers | |
instance.update( 'force' ); | |
expect( instance.numbers.length ).toBe( 8 ); | |
// page is: current +1 +1 -1 => 1 + 1 + 1 - 1 = 2 | |
instance.next().next().prev(); | |
expect( instance.current ).toBe( 2 ); | |
// now go to page 8 | |
instance.goTo(8); | |
expect( instance.items.join('') ).toBe( '-_' ); | |
// update the model, change the start to these characters: | |
var alt = 'ryebreads!'.split(''); | |
while(alt.length){ | |
instance.model[ alt.length - 1 ] = alt.pop(); | |
}; | |
instance.goTo(1); | |
instance.updateItems(); | |
expect( instance.items.join('') ).toBe('ryebreads!'); | |
// make sure it loops | |
expect( instance.current ).toBe( 1 ); | |
expect( instance.current ).toBe( instance.first ); | |
instance.prev(); | |
expect( instance.current ).toBe( 8 ); | |
expect( instance.current ).toBe( instance.last ); | |
instance.next(); | |
expect( instance.current ).toBe( 1 ); | |
}); | |
it("should not loop", function(){ | |
var instance = paginate({ | |
model: '1234567890'.split('') | |
,size: 2 | |
,loop: false | |
}); | |
expect( instance.numbers.length ).toBe( 5 ); | |
expect( instance.first ).toBe( 1 ); | |
expect( instance.last ).toBe( 5 ); | |
expect( instance.current ).toBe( 1 ); | |
expect( instance.loop ).toBe( false ); | |
instance.prev(); | |
expect( instance.current ).toBe( 1 ); | |
expect( instance.items.join('') ).toBe( '12' ); | |
instance.goTo(5); | |
expect( instance.current ).toBe( 5 ); | |
instance.next(); | |
expect( instance.current ).toBe( 5 ); | |
expect( instance.items.join('') ).toBe( '90' ); | |
instance.prev(); | |
expect( instance.current ).toBe( 4 ); | |
}); | |
}); | |
describe('paginateThis', function(){ | |
var elem, scope; | |
beforeEach(inject(function($rootScope, $compile, $injector){ | |
scope = $rootScope.$new(); | |
scope.refresh = function( options ){ | |
scope.refresh.res = scope.stuff | |
return scope.refresh; | |
}; | |
scope.refresh.then = function(fn){ | |
fn(scope.refresh.res); | |
return scope.refresh; | |
}; | |
scope.stuff = { | |
// make these look like nodes for this directive test | |
results: [1,2,3,4,5,6,7,8,9,10,11].map(function(d,i,a){ return {id: d}; }) | |
, total: 11 | |
, options: {query: {}} | |
}; | |
elem = $compile( | |
'<form paginate-this="{size:2}">' | |
+'<table><tr ng-repeat="row in paginate.items track by $index"><td>{{ row }}</td></tr></table>' | |
+'<div class="pager"><button class="prev" ng-click="paginate.prev()">Prev</button><button class="n" ng-click="paginate.goTo(page)" ng-repeat="page in paginate.numbers" ng-class="{active: page === paginate.current}">{{page}}</button><button class="next" ng-click="paginate.next()">Next</button></div>' | |
+'</form>' | |
)(scope); | |
scope.$digest(); | |
})); | |
it('should work in a generic DOM-element pagination scenario', function(){ | |
expect( scope.paginate.current ).toBe(1); | |
// buttons look right | |
expect(elem.find('div').text()).toBe('Prev123456Next'); | |
expect(elem.find('tr').length).toBe( scope.paginate.size ); | |
// loop left to wrap | |
angular.element(elem[0].querySelector('button.prev')).triggerHandler('click'); | |
scope.$digest(); | |
expect(elem[0].querySelector('button.active').textContent).toBe( scope.paginate.last.toString() ); | |
expect(elem.find('tr').length).toBe( scope.paginate.items.length ); | |
// trigger model change on query change | |
scope.stuff.options.query = {different: 1}; | |
scope.stuff.results = scope.stuff.results.slice(0, 5); | |
scope.stuff.total = scope.stuff.results.length; | |
scope.$digest(); | |
expect(elem.find('div').text()).toBe('Prev123Next'); | |
// trigger model change on sort change | |
scope.stuff.options.sort = 'asc:different'; | |
scope.stuff.results = scope.stuff.results.slice(0, 2); | |
scope.stuff.total = scope.stuff.results.length; | |
scope.$digest(); | |
expect(elem.find('div').text()).toBe('Prev1Next'); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment