Paging Lists with Knockout

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 of allData in the page computed observable means that we can use the exposed allData collection as you would any other observableArray and the computed observable gets update notifications for free.
  • We are using the subscribe method directly on both pageSize and allData to reset the current page index to zero whenever either value changes
  • We use Math.ceil to get a whole number of pages for pageCount, with a special case to return a page count of 1 where there are no items in the source data
Advertisements

21 thoughts on “Paging Lists with Knockout

  1. Mike Hollis says:

    I did something rather similar on a project I’m working on now, however the dataset could contain thousands of records so handling paging client side wasn’t an option. I modified your code to show basically how I did it and posted it into this jsfiddle: http://jsfiddle.net/BdnhL/1/. Note: The jsfiddle doesn’t work, I’m just using it to show the code.

  2. Patrick says:

    I think this is a brilliant solution , as i had rolled my own grid control, but not as elegant as this. Anyway is there a way to extend this to allow for sorting on the client also? as i can only think how to do this server side. Also do you know any resources on enterprise level Knockout/Javascript architecture using SPA. As i seem to have a lot of inline code building up on each SPA section. Anyway thank you for your help πŸ™‚

    • Hi Patrick,

      You can extend this quite easily to support client-side sorting – just add a public method that calls allData.sort() or allData.sort(function(a,b) { … }). That will cause the public page computed observable to be refreshed automatically. You can see it working here: http://jsfiddle.net/HrrxP/2/

      As far as enterprise level SPA architectures go, there’s nothing in particular that I am aware of but MS are working on something that didn’t quite make it into MVC4: http://www.asp.net/single-page-application

      • Patrick says:

        Hey Steve, thanks for getting back to we so quickly πŸ™‚ . I get what yur saying regarding general sorting, but i have bound my viewmodel to an html table (my grid) using your pagedobservable functionality and was wondering how to do generic individual column sorting on each column. In a kind of generic , one size fits all solution. Any ideas πŸ™‚

      • If you want to sort by different columns then you will probably have to write custom sorting functions as demonstrated here: http://jsfiddle.net/HrrxP/3/

        I suppose that you could write a custom binding handler to generate the table automatically, and then have that include links to sorting functions…but that’s probably outside the scope of the comment box!

      • Patrick says:

        indeed, these questions are out of scope,my apologies….. i was wondering if i could contact you directly reagrding a small issue i am having ? πŸ™‚

  3. Sean says:

    Thanks for the great solution, Steve. This works really nicely. I have encountered a strange behavior, however, and I was wondering if you had any insight. The issue only manifests in IE. I am using IE9. First, the JSFiddle example page throws errors and doesn’t work at all of me in IE9 (perhaps this is a known issue?) However, my implementation of your solution works great in IE9. Almost. The only problem I am seeing is that occasionally moving to another page requires two clicks instead of one. Again, this is only occasionally (occurs maybe after every 1-5 successful paging clicks) and only in IE. Any ideas?

    • Hi Sean, glad you like it. The jsFiddle problem seems to be because IE doesn’t like sourcing the script directly from Github, so I’ve created another fiddle here (http://jsfiddle.net/TrExG/) that has the script embedded directly and it works in IE for me (though I only have IE10).

      When you see the problem with your implementation, do you get any error messages in the console? If you create a jsFiddle that demonstrates the problem then I can take a look at it.

      • Sean says:

        Hi Steve,
        No errors when this occurs. I did identify the root cause of the problem though, and it is actually pretty simple: IE9 is not refreshing the list quickly enough. So if you click to go to the next page too quickly, before it has fully loaded the current page, it won’t respond, hence requiring the additional click. This is odd behavior considering the IE9 JavaScript engine is supposed to be very fast.

  4. Sean says:

    Hi Steve,
    Just now getting back to playing with this after diverting to something else for a while. As I noted above, your code was working great for me, or so I thought. But I appear to have an issue with my implementation. I am new to knockout, so there might just be something simple I’m missing. The paging is working great, but only the first time. It does not update properly if the data changes without refreshing the page.

    I created a basic example in jsfiddle based on your example: http://jsfiddle.net/rumremix/WjFgA/28/
    All I’ve done here is add a link that changes the array. When the array is updated, the change appears only if you navigate to the affected page after the update. And the quantity of available pages never updates.

    Making the fakeData observable doesn’t appear to help:
    http://jsfiddle.net/rumremix/WjFgA/29/

    Moving the binding to within the function that updates the array (I understand this is not the correct practice) results in good behavior the first time the link is clicked. But creates an odd multiplying effect on each entry on subsequent clicks:
    http://jsfiddle.net/rumremix/WjFgA/30/

    An array that is initially empty is most similar to my particular application:
    http://jsfiddle.net/rumremix/WjFgA/36/
    But this yields no results, even after the array is updated. That is, unless the binding statement is (improperly) moved inside the update function (yielding the same unwanted duplication side effect as http://jsfiddle.net/rumremix/WjFgA/30/)

    Does your script accommodate updating the data? Or can you suggest a good approach that achieves that?

    Thanks!

      • Sean says:

        Fantastic, Steve. Thanks for clarifying that. My initial approach was to make fakeData observable but that did not resolve the issue and created others in my application. Works great now. Here is an implementation that better approximates my scenario (no results until user input): http://jsfiddle.net/rumremix/WjFgA/40/
        Thanks for the great script and the quick reply.

      • Forrest says:

        I know this is an older post but is there a way to maintain the current page when the new data is added. I have a similar app and I don’t want the user to return to page 1 on each update.

      • not built in, but it would be simple enough to store current page before the update and then re-set it. You might have problems if that changes the number of pages though

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s