After my last post on client-side paging using Knockout I had a few people comment that it would be useful if something reusable could handle server-side paging. It’s not quite what I needed but it sounded useful so I thought I’d see what I could come up with…
Strategising the Server
Server-side paging poses a slightly different problem to paging on the client-side as it requires (obviously) some cooperation from the server. Given that the server might be implemented in any of a hundred different ways I decided to use a strategy pattern to actually load the data:
var ViewModel = function() {
this.pagedList = ko.pagedList({
loadPage: function(pageIndex, pageSize) {
//load data here
}
});
};The requirements of the strategy are:
- Accept a
pageIndexandpageSizeto be loaded - Return a
jQuery.Deferredinstance (such as that returned by$.ajax) - Resolve the deferred object with a value that contains an array
rowsproperty Assuming that the server response needs no modification (i.e. already has an arrayrowsproperty) then a paged list can be created as below:
var ViewModel = function() {
this.pagedList = ko.pagedList({
loadPage: function(pageIndex, pageSize) {
return $.getJson('/getPage?index=' + pageIndex + '&size=' + pageSize);
}
});
};Getting the Total Number of Rows
When writing this I was keen to avoid enforcing anything on the server implementation that wasn’t absolutely necessary, and one of the things I decided wasn’t needed was the total number of available rows. As the client side component cannot always know how many total rows are available, I decided that by default I would simply set the totalRows property to -1 and the pagedList will function perfectly without ever knowing how many rows are actually available - with the following limitations:
pageCountandtotalRowswill always return-1nextPagewill never prevent the user from moving to the next page, even if that page will be empty That being said, I also wanted to support those scenarios where the total number of rows is available, sototalRowscan be set in one of two ways:
Manually Set totalRows
As with any Knockout observable property the totalRows value can be set externally, and the pagedList will use whatever value is set to calculate pageCount and prevent moving beyond the final page using nextPage.
var vm = new ViewModel();
vm.pagedList.totalRows(100); //specify 100 rows totalInclude totalRows in Server Response
The minimum requirement of the data returned from the page loading strategy is that it requires an array rows property, but if it also includes an integer totalRows value then this will automatically set the observable value on the pagedList.
this.pagedList = ko.pagedList({
loadPage: function(pageIndex, pageSize) {
var defer = $.Deferred();
//this is an example of the expected response object from the server
defer.resolve({
rows: [1, 2, 3],
totalRows: 100 //specify the total number of rows in the response
});
return defer;
}
});In this way we can either specify the total number of rows on the client side, on the server side, or not at all - and still use the pagedList in the same way.
Pre-Loading the First page
When the page containing the pagedList is being generated on the server it is preferable that the first page of data already be loaded, rather than waiting on an unnecessary post back to the server to populate the list.
With that scenario in mind, I added the ability to specify the first page of data as another option on the constructor.
this.pagedList = ko.pagedList({
loadPage: function(pageIndex, pageSize) {
//...
},
firstPage: {
rows: [1, 2, 3],
totalRows: 100
}
});If this option is specified then the pagedList will not attempt to load the first page of data and will instead render the data specified in the option.
Note: this is also another mechanism by which the totalRows property can be pre-populated on the client side.
Mapping Data to ViewModels
In most cases the server will be returning something more complicated than the numeric values in the examples above, and in those cases it is common to want to wrap the returned object in a view model of it’s own.
To support this functionality I included a map constructor parameter that will be invoked on each of the rows returned from the page load strategy.
this.pagedList = ko.pagedList({
//...
map: function(item) {
return ko.mapping.fromJS(item);
}
});Note: this example makes use of the Knockout mapping plugin to generate a view model.
Bindable Properties
Much of the behaviour is similar to the pagedObservableArray as it was described in my original post, with the most significant difference being that the returned object can be used directly - there is no need to bind to a page property as there was before. It also exposes the following properties:
- pageSize - An
observableinstance containing the number of items per page - pageIndex - An
observableinstance containing the zero-based current page index - totalRows - An
observableinstance containing the total number of rows that are assumed to be available (or -1 if this has not been determined) - pageCount - A computed
observablethat returns the number of pages of data available (or -1 iftotalRowsis unknown) - previousPage - A command that moves to the previous page, if possible
- nextPage - A command that moves to the next page, if possible
- loadPage - A command that loads the page data at the index specified as the first parameter. The
loadPage.isRunningobservable should be used for all loading notifications One point to note is that this implementation does not expose anobservableArraycontaining the data; instead it exposes a regularobservablewith a value that is an array. This may seem a little strange but I felt that the additional functions and properties on the observable array do not really make sense when talking about a collection of server-specified data.
Source Code & Samples
The source code and unit tests are available on GitHub, and a working sample is available on jsFiddle.
If anyone else has any other suggestions or requests just let me know in the comments!