As covered by this StackOverflow question, Knockout can get a little bit slow when trying to render large amounts of data.
This has been particularly noticeable in a recent project that has to run on older iPads, so I took the time to put together a simple paging solution. You can see the source code (and tests) here or you can see a running example here.
To make use of this object, create an instance of Utils.PagedObservableArray
as below:
var ViewModel = function(data) {
this.pagedList = new Utils.PagedObservableArray({
data: data,
pageSize: 3
});
};
You can then bind to the exposed properties using some simple HTML:
<div data-bind="with: pagedList">
<div>
<a href="#" data-bind="click: previousPage">Previous Page</a>
<span
data-bind="text: 'Page ' + (pageIndex() + 1) + ' of ' + pageCount()"
></span>
<a href="#" data-bind="click: nextPage">Next Page</a>
</div>
<ul data-bind="foreach: page">
<li data-bind="text: $data"></li>
</ul>
</div>
The new instance wraps the existing observableArray
object from the Knockout library and exposes a number of properties to support paging:
- allData - An
observableArray
instance exposing the entire data set. This should be used to populate and to access the source data - pageSize - An
observable
instance containing the number of items per page - pageIndex - An
observable
instance containing the zero-based current page index - pageCount - A computed
observable
that returns the number of pages of data available - page - An
observableArray
instance that contains the current page of data - previousPage - A function that moves to the previous page, if possible
- nextPage - A function that moves to the next page, if possible
The final implementation is:
(function(Utils, ko) {
Utils.PagedObservableArray = function(options) {
options = options || {};
if ($.isArray(options)) options = { data: options };
var //the complete data collection
_allData = ko.observableArray(options.data || []),
//the size of the pages to display
_pageSize = ko.observable(options.pageSize || 10),
//the index of the current page
_pageIndex = ko.observable(0),
//the current page data
_page = ko.computed(function() {
var pageSize = _pageSize(),
pageIndex = _pageIndex(),
startIndex = pageSize * pageIndex,
endIndex = pageSize * (pageIndex + 1);
return _allData().slice(startIndex, endIndex);
}, this),
//the number of pages
_pageCount = ko.computed(function() {
return Math.ceil(_allData().length / _pageSize()) || 1;
}),
//move to the next page
_nextPage = function() {
if (_pageIndex() < _pageCount() - 1) _pageIndex(_pageIndex() + 1);
},
//move to the previous page
_previousPage = function() {
if (_pageIndex() > 0) _pageIndex(_pageIndex() - 1);
};
//reset page index when page size changes
_pageSize.subscribe(function() {
_pageIndex(0);
});
_allData.subscribe(function() {
_pageIndex(0);
});
//public members
this.allData = _allData;
this.pageSize = _pageSize;
this.pageIndex = _pageIndex;
this.page = _page;
this.pageCount = _pageCount;
this.nextPage = _nextPage;
this.previousPage = _previousPage;
};
})(Utils, ko);
The implementation itself is not doing anything particularly clever but there are a couple of areas to note:
- By using
jQuery.isArray
we can determine whether the parameter passed in is an array or just another object. If it is an array we just want to use it as the source data to save callers from wrapping their data in an options object. - Using
Array.slice
on the return value ofallData
in thepage
computed observable means that we can use the exposedallData
collection as you would any otherobservableArray
and the computed observable gets update notifications for free. - We are using the
subscribe
method directly on bothpageSize
andallData
to reset the current page index to zero whenever either value changes - We use
Math.ceil
to get a whole number of pages forpageCount
, with a special case to return a page count of 1 where there are no items in the source data