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
observableArrayinstance exposing the entire data set. This should be used to populate and to access the source data - pageSize - An
observableinstance containing the number of items per page - pageIndex - An
observableinstance containing the zero-based current page index - pageCount - A computed
observablethat returns the number of pages of data available - page - An
observableArrayinstance 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.isArraywe 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.sliceon the return value ofallDatain thepagecomputed observable means that we can use the exposedallDatacollection as you would any otherobservableArrayand the computed observable gets update notifications for free. - We are using the
subscribemethod directly on bothpageSizeandallDatato reset the current page index to zero whenever either value changes - We use
Math.ceilto 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