Skip to content Skip to tags Skip to twitter news Skip to blog roll Skip to categories Skip to archives Skip to recent posts

December 30, 2011

none

Pagination and Backbone.js

by in Javascript on December 30, 2011 @ 2:28 pm

I recently had to create some pagination for a backbone.js project I’ve been working on.  What I created has been working quite well but was not 100% happy with what I did.  Since I had a week off I figured I’d rewrite it as a mixin and clean up the approach.  Will have to work it back into the project somehow but that is something to worry about later.

Let’s just jump right in there.  Some caveats: I am sure there is probably a better approach to some of the code, so please give some guidance on how it should be improved.  This code assumes that all the data is already loaded into the collection.  The set of data I was working with had a record count of about 28,000 items.  I’ve gone back and both on if having that much data loaded is a good or bad thing.  Of all the articles I’ve read there doesn’t seem to be a consensus.

The folder structure I am using is:

// Folder structure being used
assets/
app/
collections/
tags.js
mixins/
pagination.js
models/
tag.js
views/
pagination.js
tags.js

The JSON structure I am dealing with:

{
"status":true,
"tags":[
{
"id":1,
"name":"A"
},...
]
}
view raw tags_all.php This Gist brought to you by GitHub.

The model, nothing too special going on here:

(function (models) {
models.Tag = Backbone.Model.extend({});
})(App.models);
view raw models_tag.js This Gist brought to you by GitHub.

The collection, again, is a pretty basic set up.  I’ve set to model to map to App.models.Tag, our url to point to tags_all.php and overridden the parse method to return a cleaned up tags array.

The last line _.extend(collections.Tags.prototype, pagination); is where we mixin in our Pagination module(?: not sure what its proper name is actually. Module, Class?).

(function (collections, pagination, model) {
collections.Tags = Backbone.Collection.extend({
model : model,
url : 'tags_all.php',

/**
* @param resp the response returned by the server
* @returns (Array) tags
*/
parse : function (resp) {
var tags = resp.tags;

return tags;
}
});

_.extend(collections.Tags.prototype, pagination);
})(App.collections, App.mixins.Pagination, App.models.Tag);

Next up is the Pagination mixin code, have a read of it and I’ll comment after the code.

(function (mixins) {
/**
* @class
* Pagination
*/
mixins.Pagination = {
/** how many items to show per page */
perPage : 20,

/** page to start off on */
page : 1,

/**
*
*/
nextPage : function () {
var self = this;

self.page = ++self.page;
self.pager();
},

previousPage : function () {
var self = this;

self.page = --self.page || 1;
self.pager();
},

goTo : function (page) {
var self = this;

self.page = parseInt(page,10);
self.pager();
},

howManyPer : function (perPage) {
var self = this;
self.page = 1;
self.perPage = perPage;
self.pager();
},

setSort : function (column, direction) {
var self = this;

self.pager(column, direction);
},

pager : function (sort, direction) {
var self = this,
start = (self.page-1)*this.perPage,
stop = start+self.perPage;

if (self.orgmodels === undefined) {
self.orgmodels = self.models;
}

self.models = self.orgmodels;

if (sort) {
self.models = self._sort(self.models, sort);
}

self.reset(
self.models.slice(start,stop)
);
},

_sort : function (models, sort) {
models = models.sort(function(a,b) {
var a = a.get(sort),
b = b.get(sort);

if (direction === 'desc') {
if (a > b) {
return -1;
}

if (a < b) {
return 1;
}
}
else {
if (a < b) {
return -1;
}

if (a > b) {
return 1;
}
}

return 0;
});

return models;
},

info : function () {
var self = this,
info = {},
totalRecords = (self.orgmodels) ? self.orgmodels.length : self.length,
totalPages = Math.ceil(totalRecords/self.perPage);

info = {
totalRecords : totalRecords,
page : self.page,
perPage : self.perPage,
totalPages : totalPages,
lastPage : totalPages,
lastPagem1 : totalPages-1,
previous : false,
next : false,
page_set : [],
startRecord : (self.page - 1) * self.perPage + 1,
endRecord : Math.min(totalRecords, self.page * self.perPage)
};

if (self.page > 1) {
info.prev = self.page - 1;
}

if (self.page < info.totalPages) {
info.next = self.page + 1;
}

info.pageSet = self.setPagination(info);

self.information = info;
return info;
},

setPagination : function (info) {
var pages = [];
// How many adjacent pages should be shown on each side?
var ADJACENT = 3;
var ADJACENTx2 = ADJACENT*2;
var LASTPAGE = Math.ceil(info.totalRecords/info.perPage);
var LPM1 = -1;

if (LASTPAGE > 1) {
//not enough pages to bother breaking it up
if (LASTPAGE < (7 + ADJACENTx2)) {
for (var i=1,l=LASTPAGE; i <= l; i++) {
pages.push(i);
}
}
// enough pages to hide some
else if (LASTPAGE > (5 + ADJACENTx2)) {

//close to beginning; only hide later pages
if (info.page < (1 + ADJACENTx2)) {
for (var i=1, l=4+ADJACENTx2; i < l; i++) {
pages.push(i);
}
}

//in middle; hide some front and some back
else if(LASTPAGE - ADJACENTx2 > info.page && info.page > ADJACENTx2) {
for (var i = info.page - ADJACENT; i <= info.page + ADJACENT; i++) {
pages.push(i);
}
}
//close to end; only hide early pages
else{
for (var i = LASTPAGE - (2 + ADJACENTx2); i <= LASTPAGE; i++) {
pages.push(i);
}
}
}
}

return pages;
}
};

})(App.mixins);

Most of the code is pretty self-explanatory, so I won’t dig into every line.  There are some methods you will see in the code, setSort and _sort, that are actually not in use for this discussion but will follow up on them in another post.

The first four methods are the ones that get triggered to start the ball rolling on the pagination.  Each of them call the pager method.

The page method makes a backup copy of the models the first pass through.  Resets models with said backup.  Ignore the sort for this post.  And lastly we pass in the start and stop points into the Array slice method, which we in turn pass to the Backbone.js reset method.  This will trigger a reset event and any view that is watching it will execute.

The last two methods to concern yourself with is the info and setPagination methods.  The info method does some base variable set up that will be used in views that need that info, our pagination view for-instance.  The setPagination method is called by info and at this point I must add two things.  The first is that I more or less borrowed the code from a PHP pagination script and can’t find it again, otherwise I’d give credit.  The second is that the purpose of setPagination is to allow for custom pagination structure and I was not clear I how I should make this more straight forward.  Any suggestions would be great.

So, what does setPagination do?  Basically it returns an array of what pages you want in your pagination — this will become a bit clearer what you see the pagination view below/in action, but let me try to explain.  In the code that is there, if we are on page 8 of our recordset, the pages that show will look something like this: 5 6 7 8 9 10 11.   But you are not limited to that structure.  In the project I will be merging this into, it will have a structure like, assuming I am on page 8 again: 1-5 6 7 8 9 10 11-15.

Hopefully that is clear.

Next two up are the pagination view and template/index page themselves.  The final structure will look something like: First Previous 5 6 7 8 9 10 11 Next Last Show 20 | 50 | 100 141 – 160 of 28091 shown”.  Again, everything should be pretty self-explanatory, each method is named to do what it is supposed to do.

(function (views) {
views.Pagination = Backbone.View.extend({

events : {
'click a.first' : 'gotoFirst',
'click a.prev' : 'gotoPrev',
'click a.next' : 'gotoNext',
'click a.last' : 'gotoLast',
'click a.page' : 'gotoPage',
'click .howmany a' : 'changeCount'
},

tagName : 'aside',
initialize : function () {
_.bindAll (this, 'render');
var self = this;

self.tmpl = _.template($('#tmpPagination').html());
self.collection.bind('reset', this.render);
$(self.el).appendTo('body');
},
render : function () {
            var self;
            self = this;

var html = this.tmpl(self.collection.info());
$(this.el).html(html);
},

gotoFirst : function (e) {
e.preventDefault();

var self = this;

self.collection.goTo(1);
},

gotoPrev : function (e) {
e.preventDefault();

var self = this;

self.collection.previousPage();
},

gotoNext : function (e) {
e.preventDefault();

var self = this;

self.collection.nextPage();
},

gotoLast : function (e) {
e.preventDefault();

var self = this;

self.collection.goTo(self.collection.information.lastPage);
},

gotoPage : function (e) {
e.preventDefault();

var self = this;
var page = $(e.target).text();

self.collection.goTo(page);
},

changeCount : function (e) {
e.preventDefault();

var self = this;
var per = $(e.target).text();

self.collection.howManyPer(per);
}
});
})(App.views);

I am using underscore.js’ template function.  Only real thing to note is the _.each loop.  This is where the “pages”, e.g. “5 6 7 8 9 10 11“, will get parsed out.

<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title></title>
<script src="jquery-1.7.1.min.js"></script>
<script src="underscore.js"></script>
<script src="json2.js"></script>
<script src="backbone.js"></script>
<script type="text/javascript">
var App = {
collections : {},
models : {},
views : {},
mixins : {},
init : function () {
var collection = new App.collections.Tags();
App.views.tags = new App.views.Tags({collection:collection});
new App.views.Pagination({collection:collection});
}
};
</script>
<script src="assets/app/mixins/pagination.js"></script>
<script src="assets/app/models/tag.js"></script>
<script src="assets/app/collections/tags.js"></script>
<script src="assets/app/views/tags.js"></script>
<script src="assets/app/views/pagination.js"></script>
<script type="text/javascript">
$(App.init);
</script>
</head>
<body>

<script type="text/html" id="tmpPagination">
<span class="cell last pages">
<% if (page != 1) { %>
<a href="#" class="first">First</a>
<a href="#" class="prev">Previous</a>
<% } %>
<% _.each (pageSet, function (p) { %>
<% if (page == p) { %>
<span class="page selected"><%= p %></span>
<% } else { %>
<a href="#" class="page"><%= p %></a>
<% } %>
<% }); %>
<% if (lastPage != page) { %>
<a href="#" class="next">Next</a>
<a href="#" class="last">Last</a>
<% } %>
</span>

<span class="cell howmany">
Show
<a href="#" class="selected">20</a>
|
<a href="#" class="">50</a>
|
<a href="#" class="">100</a>
</span>

<span class="cell first records">
<span class="current"><%= startRecord %></span>
-
<span class="perpage"><%= endRecord %></span>
of
<span class="total"><%= totalRecords %></span>
shown
</span>
</script>
</body>
</html>
view raw index.html This Gist brought to you by GitHub.

This last bit is the view that helps parses out the tags.

(function (views) {
views.Tags = Backbone.View.extend({
tagName : 'ul',
initialize : function () {
_.bindAll (this, 'render', 'addAll', 'addOne');
var self = this;
self.collection.fetch({
success : function () {
self.collection.pager();
},
silent:true
});
self.collection.bind('reset', self.addAll);
$(self.el).appendTo('body');
},
addAll : function () {
var self = this;

$(self.el).empty();
self.collection.each (self.addOne);
},

addOne : function (model) {
var self = this;

var view = new Tag({model:model});
view.render();
$(self.el).append(view.el);
}
});

var Tag = Backbone.View.extend({
tagName : 'li',
render : function () {
$(this.el).html(this.model.get('name'));
}
});
})(App.views);
view raw views_tags.js This Gist brought to you by GitHub.

Closing

Like anything else, there are a hundred ways to do things and this is just one.  My example goes off the assumption that all the data is available but I am sure it can be re-written to only fetch a certain number of records from the server on an as needed basis.

What would you do differently?  Did you see anything wrong with my implementation?

back to beginning of this post back to skip to links

If you liked this article why don't you share it:

Stumble it delicious Digg it Reddit it DZone it Bump it Mixx it! Buzz up! E-mail

Comments are closed.


Learn from my mistakes, I got burnt by the flame, you don't have to.


RSS Feed Link My Hosting of Choice

79 queries. 0.685 seconds. Powered by WordPress visitor stats