I’ve been a heavy user of Knockout JS and MVVM with JavaScript for over a year and I’ve found most designs can be achieved with the basic data-bindings. There are two scenarios that I have found which require specialized bindings. The first is a select box with numerous options. The second is a long list of data that doesn’t fit on a single screen.
I prefer to use the chosen jQuery plugin to manage unwieldy select boxes. The problem is combining the jQuery plugin with Knockout data-binding. For this I use a Knockout Binding Extension For Chosen. One caveat on this binding is that it can take a while to bind. If you have multiple select boxes with large amounts of data displaying dynamically, you can notice a significant delay. I ran into this problem when binding to an editable grid. The entire grid would display with the select boxes hidden until the user selects that row for editing. The grid would lag by up to 10 seconds while all the select boxes rendered using the chosen plugin. To speed up the grid I wrapped each select box in a span with an “if” binding. This kept the select boxes from rendering until the user actually edited that row. Since only one row’s worth of select boxes were being rendered instead of all rows, the delay wasn’t as noticeable.
The long list of data that doesn’t fit on a single screen is a much different problem. To solve this I wanted a paging solution similar to the data tables plugin for jQuery.
The important functionality that I wanted were the ability to change the page size, the page links, and of course, the actual paging. Originally, I started out building the paging mechanics directly into my view models, but this created a lot of duplication both in the view model and in the html. My next step was to pull the paging functions out of the view model and put them into a base model that could extend any view model with paging functionality. It turns out the paging functionality is easy to encapsulate; for any paged list, you need to track the current page, the number of items per page, and the total number of items. From there it’s fairly simple to calculate the items to display on the current page.
self.page = ko.observable(1); self.itemsPerPage = ko.observable(10); self.totalPages = ko.computed(function () { var array = observableArray(); return Math.ceil(array.length / self.itemsPerPage()); }); self.pagedItems = ko.computed(function () { var array = observableArray(); var indexOfFirstItemOnCurrentPage = (((self.page() * 1) - 1) * (self.itemsPerPage() * 1)); var pageArray = array.slice(indexOfFirstItemOnCurrentPage, indexOfFirstItemOnCurrentPage + (self.itemsPerPage()* 1)); return pageArray; });
The problem of repeating the html for the knockout binding isn’t solved with a view model, though. What I really wanted was a drop-in replacement for the foreach binding that would work on any list and would wrap up all the paging functionality. There is a SimpleGrid knockout binding that does paging, but it replaces the current html node with its own idea of how your data should be rendered. I wanted to use the inline template that the foreach binding uses so that I could page a table, an un-ordered list, or a bunch of divs. In order to be a true drop-in replacement for foreach, I had to separate out the three elements of the pager: the page size control, the paged items, and the page links. I call my pager binding like so:
<div id="testBinding"> <div data-bind="pageSizeControl: observableArray, pageSize: pageSize"></div> <table> <thead> <tr><th>$index</th><th>Key</th><th>Value</th><th>Page Size</th></tr> </thead> <tbody data-bind="pagedForeach: observableArray"> <tr> <td data-bind="text: $index"></td> <td data-bind="text: key"></td> <td data-bind="text: value"></td> <td class="parentPageSize" data-bind="text: $parent.pageSize"></td> </tr> </tbody> </table> <div data-bind="pageLinks: observableArray"></div> </div>
I have four bindings that I can choose from which give me detailed control over the paging while also letting me reuse the code in a modular way.
pagedForeach: displays the current page of items pageLinks: displays the first page, last page, and page number navigation links pageSizeControl: displays a select box with four standard page sizes to choose from pageSize: lets your view model control the page size directly
I checked in the binding to github as knockout.pager. I then used myGet to package the files and upload them to nuGet. I also built several unit tests. One thing you may notice is that in my bindings I attach the paging view model to the array. This is because the other bindings can see the same view model. In order to work correctly, the bindings have to interact with the same view model. Rather than calling the foreach binding from my binding, I looked at the knockout source and so that foreach is simply a wrapper for the template binding, so I call the template binding directly. I also used the same method of generating templates for the page links and pager size controls that the SimpleGrid binding used.