├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── bower.json ├── demo.html ├── example ├── server-side-paginated-view-model.coffee ├── view.jade └── view_model.coffee ├── knockout-datatable.coffee ├── knockout-datatable.css ├── knockout-datatable.js ├── knockout-datatable.less ├── knockout-datatable.min.js ├── knockout-datatable.min.js.map ├── package.json └── test ├── client-side-pagination.js ├── knockout-datatable.js └── server-side-pagination.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Knockout DataTable Changelog 2 | 3 | ## [0.7.0] - 4 | ## [0.6.3] - 5 | ## [0.6.2] - 6 | ## [0.6.1] - 7 | ## [0.6.0] - 8 | ## [0.5.1] - 9 | ## [0.5.0] - 10 | ## [v0.4.0] - 11 | ## [0.3.1] - 12 | ## [v0.3.0] - 13 | ## [0.2.0] - 14 | ## [0.1.1] - 15 | ## [0.1.0] - 16 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | * [Chris Dosé](https://github.com/doughsay) 4 | * [Christian Bankester](https://github.com/cmbankester) 5 | * [Chris McKnight](https://github.com/cmckni3) 6 | 7 | If you have made a contribution, feel free to create a pull request to add your information to this list. 8 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig { 3 | 4 | karma: 5 | unit: 6 | frameworks: ['mocha', 'chai-sinon'] 7 | browsers: [ 8 | 'Chrome' 9 | 'PhantomJS' 10 | 'Firefox' 11 | ] 12 | plugins: [ 13 | 'karma-mocha' # Use mocha for test organization 14 | 'karma-mocha-reporter' # Use mocha style of reporting 15 | 'karma-chai-sinon' # Use chai/sinon for testing helpers 16 | 'karma-firefox-launcher' 17 | 'karma-chrome-launcher' 18 | 'karma-phantomjs-launcher' 19 | ] 20 | files: [ 21 | # Require DataTable & it's dependencies 22 | {src: 'bower_components/knockout/dist/knockout.js'} 23 | {src: 'knockout-datatable.js'} 24 | 25 | # Require our tests 26 | {src: 'test/**/*.js'} 27 | ] 28 | reporters: ['mocha'] 29 | # reporters: 'dots' 30 | # runnerPort: 9999 31 | # singleRun: true 32 | # logLevel: 'ERROR' 33 | 34 | # compile coffeescript files 35 | coffee: 36 | datatable: 37 | files: 38 | 'knockout-datatable.js': 'knockout-datatable.coffee' 39 | 40 | # compile less files 41 | less: 42 | datatable: 43 | options: 44 | compress: true 45 | files: 46 | 'knockout-datatable.css': 'knockout-datatable.less' 47 | 48 | # uglifyjs files 49 | uglify: 50 | datatable: 51 | options: 52 | sourceMap: true, 53 | 54 | src: 'knockout-datatable.js' 55 | dest: 'knockout-datatable.min.js' 56 | } 57 | 58 | grunt.loadNpmTasks 'grunt-contrib-uglify' 59 | grunt.loadNpmTasks 'grunt-contrib-coffee' 60 | grunt.loadNpmTasks 'grunt-contrib-less' 61 | grunt.loadNpmTasks 'grunt-karma' 62 | 63 | grunt.registerTask('test', ['karma']) 64 | 65 | grunt.registerTask('default', [ 66 | 'coffee', 67 | 'less', 68 | 'uglify' 69 | ]) 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Immense Networks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Knockout DataTable 2 | 3 | Knockout DataTable is a flexible and reusable Knockout.js view model for data tables. 4 | 5 | ## Demo 6 | 7 | Check out the [demo](http://rawgit.com/immense/knockout-datatable/master/demo.html) to get a quick idea of how it works and how to use it. 8 | 9 | ## Installation 10 | 11 | To install it in your bower-enabled project, run `bower install knockout-datatable`. 12 | 13 | Or drop the `knockout-datatable{.min}.js` file in your vendor assets javascript folder and require it in your application. 14 | 15 | ## Usage 16 | 17 | Refer to the [demo](http://rawgit.com/immense/knockout-datatable/master/demo.html) for detailed usage instructions. 18 | 19 | ### API 20 | 21 | The following methods are available on the DataTable instance: 22 | * `prevPage()` - go to the previous page (if not on page 1) 23 | * `nextPage()` - go to next page (if not on last page) 24 | * `toggleSort(field)` 25 | - switches to ascending sort if sorted descending by `field` 26 | - switches to descending sort if sorted ascending by `field` 27 | - sorts ascending by `field` if not already sorted by `field` 28 | * `gotoPage(pageNum)` - sets the current page to `pageNum` 29 | * `pageClass(pageNum)` - returns `"active"` if `pageNum` is the current page 30 | * `addRecord(new_record)` - pushes `new_record` onto the datatable's rows 31 | * `removeRecord(record)` - removes `record` from the datatable's rows 32 | * `replaceRows(new_rows_array)` 33 | - resets the datatable's rows to `new_rows_array` 34 | - sets the current page to `1` 35 | * `forceFilter(true|false)` - enable / disable forcing filtering of the roles 36 | - tells DataTable to filter the rows even if the current filter is falsey 37 | 38 | ## Building 39 | 40 | To build the Knockout DataTable coffeescript source, do the following in a node.js enabled environment: 41 | 42 | ``` 43 | npm install -g grunt-cli 44 | npm install 45 | grunt 46 | ``` 47 | 48 | ## Running tests 49 | 50 | To run the tests, do the following in a node.js enabled environment: 51 | 52 | ``` 53 | npm install -g grunt-cli 54 | npm install 55 | grunt test 56 | ``` 57 | 58 | ## Contributing 59 | 60 | 1. Fork it 61 | 1. Create your feature branch (`git checkout -b my-new-feature`) 62 | 1. Commit your changes (`git commit -am 'Add some feature'`) 63 | 1. Push to the branch (`git push origin my-new-feature`) 64 | 1. Create new Pull Request 65 | 66 | ## Contributors 67 | 68 | See [Contributors](CONTRIBUTORS.md) 69 | 70 | ## License 71 | 72 | Knockout DataTable is released under the MIT License. Please see the LICENSE file for details. 73 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockout-datatable", 3 | "version": "0.7.2", 4 | "private": false, 5 | "main": [ 6 | "knockout-datatable.min.js", 7 | "knockout-datatable.css" 8 | ], 9 | "license": "MIT", 10 | "ignore": [ 11 | ".gitignore", 12 | "demo.html", 13 | "Gruntfile.coffee", 14 | "README.md", 15 | "example", 16 | "node_modules" 17 | ], 18 | "devDependencies": {}, 19 | "dependencies": { 20 | "knockout": "3.x" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Knockout DataTable Demo 7 | 8 | 9 | 10 | 11 | 104 | 105 | 170 | 171 | 172 |
173 |
174 | 178 |

Knockout DataTable

179 |
180 |
181 |

Knockout DataTable Demo

182 |
183 |
184 |
185 | 186 |

Simple example

187 |

S'pose we wanted to display a table of cities. Just create a view model for the data:

188 | 189 |
190 | class City
191 | 
192 |   constructor: (@view, row) ->
193 |     @population  = ko.observable row.population
194 |     @countryName = row.country_name
195 |     @cityName    = row.city_name
196 | 
197 | class @CitiesModel
198 | 
199 |   constructor: ->
200 | 
201 |     tableOptions =
202 |       recordWord:       'city'
203 |       recordWordPlural: 'cities'
204 |       sortDir:          'desc'
205 |       sortField:        'population'
206 |       perPage:          15
207 |       unsortedClass:    'glyphicon glyphicon-sort'
208 |       ascSortClass:     'glyphicon glyphicon-sort-by-attributes'
209 |       descSortClass:    'glyphicon glyphicon-sort-by-attributes-alt'
210 | 
211 |     @table = new DataTable [], tableOptions
212 |     @table.loading true
213 | 
214 |     req = new XMLHttpRequest()
215 |     req.open 'GET', '/api/cities', true
216 | 
217 |     req.onload = =>
218 |       if req.status >= 200 and req.status < 400
219 |         response = JSON.parse req.responseText
220 |         rows = response.results.map (row) => new City @, row
221 |         @table.rows rows
222 |         @table.loading false
223 |       else
224 |         alert "Error communicating with server"
225 |         @table.loading false
226 | 
227 |     req.onerror = =>
228 |       alert "Error communicating with server"
229 |       @table.loading false
230 | 
231 |     req.send()
232 | 
233 |     ko.applyBindings @
234 | 235 |

And a table, like so:

236 | 237 |
238 | <div data-bind="with: table">
239 |   <div class="pull-right">
240 |     <strong>Results per page</strong>
241 |     <select data-bind="options: [10,25,50], value: perPage"></select>
242 |   </div>
243 |   <input type="text" data-bind="textInput: filter" placeholder="Search"/>
244 |   <table class="table table-striped table-bordered">
245 |     <thead>
246 |       <tr>
247 |         <th style="width: 34%;" data-bind="click: toggleSort('cityName')" class="sortable">
248 |           City
249 |           <i data-bind="css: sortClass('cityName')"></i>
250 |         </th>
251 |         <th style="width: 33%;" data-bind="click: toggleSort('countryName')" class="sortable">
252 |           Country
253 |           <i data-bind="css: sortClass('countryName')"></i>
254 |         </th>
255 |         <th style="width: 33%;" data-bind="click: toggleSort('population')" class="sortable">
256 |           Population
257 |           <i data-bind="css: sortClass('population')"></i>
258 |         </th>
259 |       </tr>
260 |     </thead>
261 |     <tbody>
262 |       <tr data-bind="visible: showNoData">
263 |         <td colspan="3" class="aligncenter">
264 |           This table has no data.
265 |         </td>
266 |       </tr>
267 |       <tr data-bind="visible: showLoading">
268 |         <td colspan="3" class="aligncenter">
269 |           <i data-bind="css: {'icon-spin': showLoading}" class="icon-spinner"></i>
270 |           Loading data...
271 |         </td>
272 |       </tr>
273 |       <!-- ko foreach: {data: pagedRows, as: '$row'}  -->
274 |       <tr>
275 |         <td data-bind="text: $row.cityName"></td>
276 |         <td data-bind="text: $row.countryName"></td>
277 |         <td data-bind="text: $row.population"></td>
278 |       </tr>
279 |       <!-- /ko -->
280 |     </tbody>
281 |   </table>
282 |   <span data-bind="text: recordsText" class="label label-info pull-right"></span>
283 |   <div data-bind="visible: pages() > 1">
284 |     <ul class="pagination">
285 |       <li data-bind="css: leftPagerClass, click: prevPage">
286 |         <a href="#">&laquo;</a>
287 |       </li>
288 |       <!-- ko foreach: {data: (new Array(pages()))} -->
289 |       <li data-bind="css: $parent.pageClass($index() + 1)">
290 |         <a href="#" data-bind="text: $index() + 1, click: $parent.gotoPage($index() + 1)"></a>
291 |       </li>
292 |       <!-- /ko -->
293 |       <li data-bind="css: rightPagerClass, click: nextPage">
294 |         <a href="#">&raquo;</a>
295 |       </li>
296 |     </ul>
297 |   </div>
298 | </div>
299 | <script type="text/javascript">
300 | document.addEventListener('DOMContentLoaded', function(){
301 |   new CitiesModel();
302 | });
303 | </script>
304 | 305 |

Result:

306 |
307 |
308 | Results per page 309 | 310 |
311 | 312 | 313 | 314 | 315 | 319 | 323 | 327 | 328 | 329 | 330 | 331 | 334 | 335 | 336 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 |
316 | City 317 | 318 | 320 | Country 321 | 322 | 324 | Population 325 | 326 |
332 | This table has no data. 333 |
337 | 338 | Loading data... 339 |
350 | 351 |
352 |
    353 |
  • 354 | « 355 |
  • 356 | 357 |
  • 358 | 359 |
  • 360 | 361 |
  • 362 | » 363 |
  • 364 |
365 |
366 |
367 | 372 | 373 | 374 |

Example without sorting

375 |
376 |
377 | Results per page 378 | 379 |
380 | 381 | 382 | 383 | 384 | 387 | 390 | 393 | 394 | 395 | 396 | 397 | 400 | 401 | 402 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 |
385 | City 386 | 388 | Country 389 | 391 | Population 392 |
398 | This table has no data. 399 |
403 | 404 | Loading data... 405 |
416 | 417 |
418 |
    419 |
  • 420 | « 421 |
  • 422 | 423 |
  • 424 | 425 |
  • 426 | 427 |
  • 428 | » 429 |
  • 430 |
431 |
432 |
433 | 438 | 439 | 440 |

Options

441 |

When instanciating with new DataTable you have can pass in the following options as the second parameter:

442 |
443 |
recordWord
444 |
The name of your rows. In the case above, we used city. Default: record
445 | 446 |
recordWordPlural
447 |
The plural name of your rows. Since we used city as our recordWord, we used cities for recordWordPlural. Default: recordWord + 's'
448 | 449 |
sortDir
450 |
The initial sorting direction for the table. Default: 'asc'
451 | 452 |
sortField
453 |
The initial sorting column for the table. As of v0.5.0, this setting is optional and the order of table.rows will be maintained and sorting will be disabled.
454 | 455 |
perPage
456 |
Integer indicating the number of rows to be shown per page. Default: 15
457 | 458 |
unsortedClass
descSortClass
ascSortClass
459 |
The classes given to the icons in the th elements indicating the direction of sorting. Set to '' if you would rather have no icons. Default: '' for each
460 |
461 |

Additionally, you can define the match function on the row class, and the datatable will use it for filtering. If left undefined (as in the example above), the DataTable will automatically search all columns defined on the row. E.g:

462 |
463 |
row.match:
464 |
465 | (filter) ->
466 |   @population().toLowerCase().indexOf(filter) >= 0 or
467 |   @countryName .toLowerCase().indexOf(filter) >= 0 or
468 |   @cityName    .toLowerCase().indexOf(filter) >= 0
469 |
470 | 471 |

Further Usage

472 |

Knockout DataTable comes packaged with some advanced filtering. Below is a list of example search terms and the results returned.

473 |
474 |
cityName:atlanta
475 |
Results with 'atlanta' in cityName (case insensitive)
476 |
cItYnAmE:aTlAnTa
477 |
Results with 'atlanta' in cityName (case insensitive)
478 |
countryName:United cityName:L
479 |
Results with 'united' in countryName and 'l' in cityName (case insensitive)

Note: as of right now, there is no built-in support for multi-word searching with ':'-delimeted searching

480 |
countryname:japan 6
481 |
Results with 'japan' in countryName and '6' somewhere in one of the columns (case insensitive)
482 |
483 |
484 |
485 |
486 | 487 | 488 | -------------------------------------------------------------------------------- /example/server-side-paginated-view-model.coffee: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immense/knockout-datatable/36f20c8813ca0abd479311919d2b18a5641fd7a9/example/server-side-paginated-view-model.coffee -------------------------------------------------------------------------------- /example/view.jade: -------------------------------------------------------------------------------- 1 | mixin th-sortable(field) 2 | th.sortable(data-bind="click: toggleSort('#{field}')") 3 | block 4 | |  5 | i(data-bind="css: sortClass('#{field}')") 6 | 7 | p.aligncenter.bindingloader 8 | i.icon-spinner.icon-spin 9 | | Loading... 10 | 11 | div.cloak(data-bind='with: exampleTable') 12 | input(type='text', data-bind='textInput: filter') 13 | table.table.table-striped 14 | thead 15 | tr 16 | +th-sortable('foo') Foo 17 | +th-sortable('bar') Bar 18 | th Baz 19 | tbody 20 | tr(data-bind='visible: showNoData') 21 | td.aligncenter(colspan=3) This table has no data. 22 | tr(data-bind='visible: showLoading') 23 | td.aligncenter(colspan=3) 24 | i.icon-spinner(data-bind='css: {"icon-spin": showLoading}') 25 | | Loading data... 26 | tr(data-bind='repeat: { foreach: pagedRows, item: "$row" }') 27 | td(data-bind='text: $row().foo') 28 | td(data-bind='text: $row().bar') 29 | td(data-bind='text: $row().baz') 30 | 31 | span.label.label-info.pull-right(data-bind='text: recordsText') 32 | 33 | .pagination(data-bind='visible: pages() > 1') 34 | ul 35 | li(data-bind='css: leftPagerClass, click: prevPage'): a(href='#') « 36 | li(data-bind='repeat: {count: pages, bind: "css: pageClass($index + 1)"}') 37 | a(href='#', data-bind='text: $index + 1, click: gotoPage($index + 1)') 38 | li(data-bind='css: rightPagerClass, click: nextPage'): a(href='#') » 39 | 40 | :coffeescript 41 | $ -> window.view = new ExampleModel 42 | -------------------------------------------------------------------------------- /example/view_model.coffee: -------------------------------------------------------------------------------- 1 | # a row in the datatable 2 | class Row 3 | 4 | match: (filter) -> 5 | @foo().toLowerCase().indexOf(filter) >= 0 or 6 | @bar().toLowerCase().indexOf(filter) >= 0 or 7 | @baz .toLowerCase().indexOf(filter) >= 0 8 | 9 | constructor: (@view, row) -> 10 | @foo = ko.observable row.foo 11 | @bar = ko.observable row.bar 12 | @baz = row.baz 13 | 14 | class @ExampleModel 15 | 16 | constructor: -> 17 | 18 | tableOptions = 19 | recordWord: 'thing' 20 | recordWordPlural: 'snakes' # This is optional. If left blank, the datatable will just append an 's' to recordWord 21 | sortDir: 'desc' 22 | sortField: 'foo' 23 | perPage: 15 24 | 25 | @exampleTable = new DataTable [], tableOptions 26 | @exampleTable.loading true 27 | 28 | req = new XMLHttpRequest() 29 | req.open 'GET', '/api/cities', true 30 | 31 | req.onload = => 32 | if req.status >= 200 and req.status < 400 33 | response = JSON.parse req.responseText 34 | rows = response.results.map (row) => new City @, row 35 | @table.rows rows 36 | @table.loading false 37 | else 38 | alert "Error communicating with server" 39 | @table.loading false 40 | 41 | req.onerror = => 42 | alert "Error communicating with server" 43 | @table.loading false 44 | 45 | req.send() 46 | 47 | ko.applyBindings @ 48 | $('.cloak').removeClass 'cloak' 49 | $('.bindingloader').remove() 50 | -------------------------------------------------------------------------------- /knockout-datatable.coffee: -------------------------------------------------------------------------------- 1 | class window.DataTable 2 | 3 | pureComputed = ko.pureComputed or ko.computed 4 | 5 | primitiveCompare = (item1, item2) -> 6 | if not item2? 7 | not item1? 8 | else if item1? 9 | if typeof item1 is 'boolean' 10 | item1 is item2 11 | else 12 | item1.toString().toLowerCase().indexOf(item2.toString().toLowerCase()) >= 0 or item1 is item2 13 | else 14 | false 15 | 16 | constructor: (rows, options) -> 17 | 18 | if not options 19 | unless rows instanceof Array 20 | options = rows 21 | rows = [] 22 | else 23 | options = {} 24 | 25 | # set some default options if none were passed in 26 | @options = 27 | recordWord: options.recordWord or 'record' 28 | recordWordPlural: options.recordWordPlural 29 | sortDir: options.sortDir or 'asc' 30 | sortField: options.sortField or undefined 31 | perPage: options.perPage or 15 32 | filterFn: options.filterFn or undefined 33 | unsortedClass: options.unsortedClass or '' 34 | descSortClass: options.descSortClass or '' 35 | ascSortClass: options.ascSortClass or '' 36 | 37 | @initObservables() 38 | 39 | if (serverSideOpts = options.serverSidePagination) and serverSideOpts.enabled 40 | unless serverSideOpts.path and serverSideOpts.loader 41 | throw new Error("`path` or `loader` missing from `serverSidePagination` object") 42 | @options.paginationPath = serverSideOpts.path 43 | @options.resultHandlerFn = serverSideOpts.loader 44 | 45 | # if server-side pagination enabled, we don't care about the initial rows 46 | @initWithServerSidePagination() 47 | 48 | else 49 | @initWithClientSidePagination(rows) 50 | 51 | initObservables: -> 52 | @sortDir = ko.observable @options.sortDir 53 | @sortField = ko.observable @options.sortField 54 | @perPage = ko.observable @options.perPage 55 | @currentPage = ko.observable 1 56 | @filter = ko.observable '' 57 | @loading = ko.observable false 58 | @rows = ko.observableArray [] 59 | 60 | initWithClientSidePagination: (rows) -> 61 | @filtering = ko.observable false 62 | @forceFilter = ko.observable false 63 | 64 | @filter.subscribe => @currentPage 1 65 | @perPage.subscribe => @currentPage 1 66 | 67 | @rows rows 68 | 69 | @rowAttributeMap = pureComputed => 70 | rows = @rows() 71 | attrMap = {} 72 | 73 | if rows.length > 0 74 | row = rows[0] 75 | (attrMap[key.toLowerCase()] = key) for key of row when row.hasOwnProperty(key) 76 | 77 | attrMap 78 | 79 | @filteredRows = pureComputed => 80 | @filtering true 81 | filter = @filter() 82 | 83 | rows = @rows.slice(0) 84 | 85 | if @forceFilter() or (filter? and filter isnt '') 86 | filterFn = @filterFn(filter) 87 | rows = rows.filter(filterFn) 88 | 89 | if @sortField()? and @sortField() isnt '' 90 | rows.sort (a,b) => 91 | aVal = ko.utils.peekObservable a[@sortField()] 92 | bVal = ko.utils.peekObservable b[@sortField()] 93 | if typeof aVal is 'string' then aVal = aVal.toLowerCase() 94 | if typeof bVal is 'string' then bVal = bVal.toLowerCase() 95 | if @sortDir() is 'asc' 96 | if aVal < bVal or aVal is '' or not aVal? then -1 else (if aVal > bVal or bVal is '' or not bVal? then 1 else 0) 97 | else 98 | if aVal < bVal or aVal is '' or not aVal? then 1 else (if aVal > bVal or bVal is '' or not bVal? then -1 else 0) 99 | else 100 | rows 101 | 102 | @filtering false 103 | 104 | rows 105 | 106 | .extend {rateLimit: 50, method: 'notifyWhenChangesStop'} 107 | 108 | @pagedRows = pureComputed => 109 | pageIndex = @currentPage() - 1 110 | perPage = @perPage() 111 | @filteredRows().slice pageIndex * perPage, (pageIndex+1) * perPage 112 | 113 | @pages = pureComputed => Math.ceil @filteredRows().length / @perPage() 114 | 115 | @leftPagerClass = pureComputed => 'disabled' if @currentPage() is 1 116 | @rightPagerClass = pureComputed => 'disabled' if @currentPage() is @pages() 117 | 118 | # info 119 | @total = pureComputed => @filteredRows().length 120 | @from = pureComputed => (@currentPage() - 1) * @perPage() + 1 121 | @to = pureComputed => 122 | to = @currentPage() * @perPage() 123 | if to > @total() 124 | @total() 125 | else 126 | to 127 | 128 | @recordsText = pureComputed => 129 | pages = @pages() 130 | total = @total() 131 | from = @from() 132 | to = @to() 133 | recordWord = @options.recordWord 134 | recordWordPlural = @options.recordWordPlural or recordWord + 's' 135 | if pages > 1 136 | "#{from} to #{to} of #{total} #{recordWordPlural}" 137 | else 138 | "#{total} #{if total > 1 or total is 0 then recordWordPlural else recordWord}" 139 | 140 | # state info 141 | @showNoData = pureComputed => @pagedRows().length is 0 and not @loading() 142 | @showLoading = pureComputed => @loading() 143 | 144 | # sort arrows 145 | @sortClass = (column) => 146 | pureComputed => 147 | if @sortField() is column 148 | 'sorted ' + 149 | if @sortDir() is 'asc' 150 | @options.ascSortClass 151 | else 152 | @options.descSortClass 153 | else 154 | @options.unsortedClass 155 | 156 | @addRecord = (record) => @rows.push record 157 | 158 | @removeRecord = (record) => 159 | @rows.remove record 160 | if @pagedRows().length is 0 161 | @prevPage() 162 | 163 | @replaceRows = (rows) => 164 | @rows rows 165 | @currentPage 1 166 | @filter undefined 167 | 168 | _defaultMatch = (filter, row, attrMap) -> 169 | (val for key, val of attrMap).some (val) -> 170 | primitiveCompare((if ko.isObservable(row[val]) then row[val]() else row[val]), filter) 171 | 172 | @filterFn = @options.filterFn or (filter_text) => 173 | # Split up filterVar into :-based conditionals and a filter 174 | filterVar = if not filter_text? then "" else filter_text 175 | [filter, specials] = [[],{}] 176 | filterVar.split(' ').forEach (word) -> 177 | if word.indexOf(':') >= 0 178 | words = word.split(':') 179 | specials[words[0]] = switch words[1].toLowerCase() 180 | when 'yes', 'true' then true 181 | when 'no', 'false' then false 182 | when 'blank', 'none', 'null', 'undefined' then undefined 183 | else words[1].toLowerCase() 184 | else 185 | filter.push word 186 | filter = filter.join(' ') 187 | return (row) => 188 | conditionals = for key, val of specials 189 | do (key, val) => 190 | if rowAttr = @rowAttributeMap()[key.toLowerCase()] # If the current key (lowercased) is in the attr map 191 | primitiveCompare((if ko.isObservable(row[rowAttr]) then row[rowAttr]() else row[rowAttr]), val) 192 | else # if the current instance doesn't have the "key" attribute, return false (i.e., it's not a match) 193 | false 194 | (false not in conditionals) and (if row.match? then row.match(filter) else _defaultMatch(filter, row, @rowAttributeMap())) 195 | 196 | initWithServerSidePagination: -> 197 | _getDataFromServer = (data, cb) => 198 | url = "#{@options.paginationPath}?#{("#{encodeURIComponent(key)}=#{encodeURIComponent(val)}" for key, val of data).join('&')}" 199 | 200 | req = new XMLHttpRequest() 201 | req.open 'GET', url, true 202 | req.setRequestHeader 'Content-Type', 'application/json' 203 | 204 | req.onload = => 205 | if req.status >= 200 and req.status < 400 206 | cb null, JSON.parse(req.responseText) 207 | else 208 | cb new Error("Error communicating with server") 209 | 210 | req.onerror = => cb new Error "Error communicating with server" 211 | 212 | req.send() 213 | 214 | _gatherData = (perPage, currentPage, filter, sortDir, sortField) -> 215 | data = 216 | perPage: perPage 217 | page: currentPage 218 | 219 | if filter? and filter isnt '' 220 | data.filter = filter 221 | 222 | if sortDir? and sortDir isnt '' and sortField? and sortField isnt '' 223 | data.sortDir = sortDir 224 | data.sortBy = sortField 225 | 226 | return data 227 | 228 | @filtering = ko.observable false 229 | @pagedRows = ko.observableArray [] 230 | @numFilteredRows = ko.observable 0 231 | 232 | @filter.subscribe => @currentPage 1 233 | @perPage.subscribe => @currentPage 1 234 | 235 | ko.computed => 236 | @loading true 237 | @filtering true 238 | 239 | data = _gatherData @perPage(), @currentPage(), @filter(), @sortDir(), @sortField() 240 | 241 | _getDataFromServer data, (err, response) => 242 | @loading false 243 | @filtering false 244 | if err then return console.log err 245 | 246 | {total, results} = response 247 | @numFilteredRows total 248 | @pagedRows results.map(@options.resultHandlerFn) 249 | 250 | .extend {rateLimit: 500, method: 'notifyWhenChangesStop'} 251 | 252 | @pages = pureComputed => Math.ceil @numFilteredRows() / @perPage() 253 | 254 | @leftPagerClass = pureComputed => 'disabled' if @currentPage() is 1 255 | @rightPagerClass = pureComputed => 'disabled' if @currentPage() is @pages() 256 | 257 | # info 258 | @from = pureComputed => (@currentPage() - 1) * @perPage() + 1 259 | @to = pureComputed => 260 | to = @currentPage() * @perPage() 261 | if to > (total = @numFilteredRows()) 262 | total 263 | else 264 | to 265 | 266 | @recordsText = pureComputed => 267 | pages = @pages() 268 | total = @numFilteredRows() 269 | from = @from() 270 | to = @to() 271 | recordWord = @options.recordWord 272 | recordWordPlural = @options.recordWordPlural or recordWord + 's' 273 | if pages > 1 274 | "#{from} to #{to} of #{total} #{recordWordPlural}" 275 | else 276 | "#{total} #{if total > 1 or total is 0 then recordWordPlural else recordWord}" 277 | 278 | # state info 279 | @showNoData = pureComputed => @pagedRows().length is 0 and not @loading() 280 | @showLoading = pureComputed => @loading() 281 | 282 | # sort arrows 283 | @sortClass = (column) => 284 | pureComputed => 285 | if @sortField() is column 286 | 'sorted ' + 287 | if @sortDir() is 'asc' 288 | @options.ascSortClass 289 | else 290 | @options.descSortClass 291 | else 292 | @options.unsortedClass 293 | 294 | @addRecord = -> 295 | throw new Error("#addRecord() not applicable with serverSidePagination enabled") 296 | 297 | @removeRecord = -> 298 | throw new Error("#removeRecord() not applicable with serverSidePagination enabled") 299 | 300 | @replaceRows = -> 301 | throw new Error("#replaceRows() not applicable with serverSidePagination enabled") 302 | 303 | @refreshData = => 304 | @loading true 305 | @filtering true 306 | 307 | data = _gatherData @perPage(), @currentPage(), @filter(), @sortDir(), @sortField() 308 | 309 | _getDataFromServer data, (err, response) => 310 | @loading false 311 | @filtering false 312 | if err then return console.log err 313 | 314 | {total, results} = response 315 | @numFilteredRows total 316 | @pagedRows results.map(@options.resultHandlerFn) 317 | 318 | toggleSort: (field) -> => 319 | @currentPage 1 320 | if @sortField() is field 321 | @sortDir if @sortDir() is 'asc' then 'desc' else 'asc' 322 | else 323 | @sortDir @options.sortDir 324 | @sortField field 325 | 326 | prevPage: -> 327 | page = @currentPage() 328 | if page isnt 1 329 | @currentPage page - 1 330 | 331 | nextPage: -> 332 | page = @currentPage() 333 | if page isnt @pages() 334 | @currentPage page + 1 335 | 336 | gotoPage: (page) -> => @currentPage page 337 | 338 | pageClass: (page) -> pureComputed => 'active' if @currentPage() is page 339 | -------------------------------------------------------------------------------- /knockout-datatable.css: -------------------------------------------------------------------------------- 1 | .no-select{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}table thead tr th.sortable{cursor:pointer;position:relative;padding-right:12px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}table thead tr th.sortable i{color:#C5C5C5;position:absolute;top:12px;right:2px}table thead tr th.sortable i.sorted{color:black} -------------------------------------------------------------------------------- /knockout-datatable.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 3 | 4 | window.DataTable = (function() { 5 | var primitiveCompare, pureComputed; 6 | 7 | pureComputed = ko.pureComputed || ko.computed; 8 | 9 | primitiveCompare = function(item1, item2) { 10 | if (item2 == null) { 11 | return item1 == null; 12 | } else if (item1 != null) { 13 | if (typeof item1 === 'boolean') { 14 | return item1 === item2; 15 | } else { 16 | return item1.toString().toLowerCase().indexOf(item2.toString().toLowerCase()) >= 0 || item1 === item2; 17 | } 18 | } else { 19 | return false; 20 | } 21 | }; 22 | 23 | function DataTable(rows, options) { 24 | var serverSideOpts; 25 | if (!options) { 26 | if (!(rows instanceof Array)) { 27 | options = rows; 28 | rows = []; 29 | } else { 30 | options = {}; 31 | } 32 | } 33 | this.options = { 34 | recordWord: options.recordWord || 'record', 35 | recordWordPlural: options.recordWordPlural, 36 | sortDir: options.sortDir || 'asc', 37 | sortField: options.sortField || void 0, 38 | perPage: options.perPage || 15, 39 | filterFn: options.filterFn || void 0, 40 | unsortedClass: options.unsortedClass || '', 41 | descSortClass: options.descSortClass || '', 42 | ascSortClass: options.ascSortClass || '' 43 | }; 44 | this.initObservables(); 45 | if ((serverSideOpts = options.serverSidePagination) && serverSideOpts.enabled) { 46 | if (!(serverSideOpts.path && serverSideOpts.loader)) { 47 | throw new Error("`path` or `loader` missing from `serverSidePagination` object"); 48 | } 49 | this.options.paginationPath = serverSideOpts.path; 50 | this.options.resultHandlerFn = serverSideOpts.loader; 51 | this.initWithServerSidePagination(); 52 | } else { 53 | this.initWithClientSidePagination(rows); 54 | } 55 | } 56 | 57 | DataTable.prototype.initObservables = function() { 58 | this.sortDir = ko.observable(this.options.sortDir); 59 | this.sortField = ko.observable(this.options.sortField); 60 | this.perPage = ko.observable(this.options.perPage); 61 | this.currentPage = ko.observable(1); 62 | this.filter = ko.observable(''); 63 | this.loading = ko.observable(false); 64 | return this.rows = ko.observableArray([]); 65 | }; 66 | 67 | DataTable.prototype.initWithClientSidePagination = function(rows) { 68 | var _defaultMatch; 69 | this.filtering = ko.observable(false); 70 | this.forceFilter = ko.observable(false); 71 | this.filter.subscribe((function(_this) { 72 | return function() { 73 | return _this.currentPage(1); 74 | }; 75 | })(this)); 76 | this.perPage.subscribe((function(_this) { 77 | return function() { 78 | return _this.currentPage(1); 79 | }; 80 | })(this)); 81 | this.rows(rows); 82 | this.rowAttributeMap = pureComputed((function(_this) { 83 | return function() { 84 | var attrMap, key, row; 85 | rows = _this.rows(); 86 | attrMap = {}; 87 | if (rows.length > 0) { 88 | row = rows[0]; 89 | for (key in row) { 90 | if (row.hasOwnProperty(key)) { 91 | attrMap[key.toLowerCase()] = key; 92 | } 93 | } 94 | } 95 | return attrMap; 96 | }; 97 | })(this)); 98 | this.filteredRows = pureComputed((function(_this) { 99 | return function() { 100 | var filter, filterFn; 101 | _this.filtering(true); 102 | filter = _this.filter(); 103 | rows = _this.rows.slice(0); 104 | if (_this.forceFilter() || ((filter != null) && filter !== '')) { 105 | filterFn = _this.filterFn(filter); 106 | rows = rows.filter(filterFn); 107 | } 108 | if ((_this.sortField() != null) && _this.sortField() !== '') { 109 | rows.sort(function(a, b) { 110 | var aVal, bVal; 111 | aVal = ko.utils.peekObservable(a[_this.sortField()]); 112 | bVal = ko.utils.peekObservable(b[_this.sortField()]); 113 | if (typeof aVal === 'string') { 114 | aVal = aVal.toLowerCase(); 115 | } 116 | if (typeof bVal === 'string') { 117 | bVal = bVal.toLowerCase(); 118 | } 119 | if (_this.sortDir() === 'asc') { 120 | if (aVal < bVal || aVal === '' || (aVal == null)) { 121 | return -1; 122 | } else { 123 | if (aVal > bVal || bVal === '' || (bVal == null)) { 124 | return 1; 125 | } else { 126 | return 0; 127 | } 128 | } 129 | } else { 130 | if (aVal < bVal || aVal === '' || (aVal == null)) { 131 | return 1; 132 | } else { 133 | if (aVal > bVal || bVal === '' || (bVal == null)) { 134 | return -1; 135 | } else { 136 | return 0; 137 | } 138 | } 139 | } 140 | }); 141 | } else { 142 | rows; 143 | } 144 | _this.filtering(false); 145 | return rows; 146 | }; 147 | })(this)).extend({ 148 | rateLimit: 50, 149 | method: 'notifyWhenChangesStop' 150 | }); 151 | this.pagedRows = pureComputed((function(_this) { 152 | return function() { 153 | var pageIndex, perPage; 154 | pageIndex = _this.currentPage() - 1; 155 | perPage = _this.perPage(); 156 | return _this.filteredRows().slice(pageIndex * perPage, (pageIndex + 1) * perPage); 157 | }; 158 | })(this)); 159 | this.pages = pureComputed((function(_this) { 160 | return function() { 161 | return Math.ceil(_this.filteredRows().length / _this.perPage()); 162 | }; 163 | })(this)); 164 | this.leftPagerClass = pureComputed((function(_this) { 165 | return function() { 166 | if (_this.currentPage() === 1) { 167 | return 'disabled'; 168 | } 169 | }; 170 | })(this)); 171 | this.rightPagerClass = pureComputed((function(_this) { 172 | return function() { 173 | if (_this.currentPage() === _this.pages()) { 174 | return 'disabled'; 175 | } 176 | }; 177 | })(this)); 178 | this.total = pureComputed((function(_this) { 179 | return function() { 180 | return _this.filteredRows().length; 181 | }; 182 | })(this)); 183 | this.from = pureComputed((function(_this) { 184 | return function() { 185 | return (_this.currentPage() - 1) * _this.perPage() + 1; 186 | }; 187 | })(this)); 188 | this.to = pureComputed((function(_this) { 189 | return function() { 190 | var to; 191 | to = _this.currentPage() * _this.perPage(); 192 | if (to > _this.total()) { 193 | return _this.total(); 194 | } else { 195 | return to; 196 | } 197 | }; 198 | })(this)); 199 | this.recordsText = pureComputed((function(_this) { 200 | return function() { 201 | var from, pages, recordWord, recordWordPlural, to, total; 202 | pages = _this.pages(); 203 | total = _this.total(); 204 | from = _this.from(); 205 | to = _this.to(); 206 | recordWord = _this.options.recordWord; 207 | recordWordPlural = _this.options.recordWordPlural || recordWord + 's'; 208 | if (pages > 1) { 209 | return from + " to " + to + " of " + total + " " + recordWordPlural; 210 | } else { 211 | return total + " " + (total > 1 || total === 0 ? recordWordPlural : recordWord); 212 | } 213 | }; 214 | })(this)); 215 | this.showNoData = pureComputed((function(_this) { 216 | return function() { 217 | return _this.pagedRows().length === 0 && !_this.loading(); 218 | }; 219 | })(this)); 220 | this.showLoading = pureComputed((function(_this) { 221 | return function() { 222 | return _this.loading(); 223 | }; 224 | })(this)); 225 | this.sortClass = (function(_this) { 226 | return function(column) { 227 | return pureComputed(function() { 228 | if (_this.sortField() === column) { 229 | return 'sorted ' + (_this.sortDir() === 'asc' ? _this.options.ascSortClass : _this.options.descSortClass); 230 | } else { 231 | return _this.options.unsortedClass; 232 | } 233 | }); 234 | }; 235 | })(this); 236 | this.addRecord = (function(_this) { 237 | return function(record) { 238 | return _this.rows.push(record); 239 | }; 240 | })(this); 241 | this.removeRecord = (function(_this) { 242 | return function(record) { 243 | _this.rows.remove(record); 244 | if (_this.pagedRows().length === 0) { 245 | return _this.prevPage(); 246 | } 247 | }; 248 | })(this); 249 | this.replaceRows = (function(_this) { 250 | return function(rows) { 251 | _this.rows(rows); 252 | _this.currentPage(1); 253 | return _this.filter(void 0); 254 | }; 255 | })(this); 256 | _defaultMatch = function(filter, row, attrMap) { 257 | var key, val; 258 | return ((function() { 259 | var results1; 260 | results1 = []; 261 | for (key in attrMap) { 262 | val = attrMap[key]; 263 | results1.push(val); 264 | } 265 | return results1; 266 | })()).some(function(val) { 267 | return primitiveCompare((ko.isObservable(row[val]) ? row[val]() : row[val]), filter); 268 | }); 269 | }; 270 | return this.filterFn = this.options.filterFn || (function(_this) { 271 | return function(filter_text) { 272 | var filter, filterVar, ref, specials; 273 | filterVar = filter_text == null ? "" : filter_text; 274 | ref = [[], {}], filter = ref[0], specials = ref[1]; 275 | filterVar.split(' ').forEach(function(word) { 276 | var words; 277 | if (word.indexOf(':') >= 0) { 278 | words = word.split(':'); 279 | return specials[words[0]] = (function() { 280 | switch (words[1].toLowerCase()) { 281 | case 'yes': 282 | case 'true': 283 | return true; 284 | case 'no': 285 | case 'false': 286 | return false; 287 | case 'blank': 288 | case 'none': 289 | case 'null': 290 | case 'undefined': 291 | return void 0; 292 | default: 293 | return words[1].toLowerCase(); 294 | } 295 | })(); 296 | } else { 297 | return filter.push(word); 298 | } 299 | }); 300 | filter = filter.join(' '); 301 | return function(row) { 302 | var conditionals, key, val; 303 | conditionals = (function() { 304 | var results1; 305 | results1 = []; 306 | for (key in specials) { 307 | val = specials[key]; 308 | results1.push((function(_this) { 309 | return function(key, val) { 310 | var rowAttr; 311 | if (rowAttr = _this.rowAttributeMap()[key.toLowerCase()]) { 312 | return primitiveCompare((ko.isObservable(row[rowAttr]) ? row[rowAttr]() : row[rowAttr]), val); 313 | } else { 314 | return false; 315 | } 316 | }; 317 | })(this)(key, val)); 318 | } 319 | return results1; 320 | }).call(_this); 321 | return (indexOf.call(conditionals, false) < 0) && (row.match != null ? row.match(filter) : _defaultMatch(filter, row, _this.rowAttributeMap())); 322 | }; 323 | }; 324 | })(this); 325 | }; 326 | 327 | DataTable.prototype.initWithServerSidePagination = function() { 328 | var _gatherData, _getDataFromServer; 329 | _getDataFromServer = (function(_this) { 330 | return function(data, cb) { 331 | var key, req, url, val; 332 | url = _this.options.paginationPath + "?" + (((function() { 333 | var results1; 334 | results1 = []; 335 | for (key in data) { 336 | val = data[key]; 337 | results1.push((encodeURIComponent(key)) + "=" + (encodeURIComponent(val))); 338 | } 339 | return results1; 340 | })()).join('&')); 341 | req = new XMLHttpRequest(); 342 | req.open('GET', url, true); 343 | req.setRequestHeader('Content-Type', 'application/json'); 344 | req.onload = function() { 345 | if (req.status >= 200 && req.status < 400) { 346 | return cb(null, JSON.parse(req.responseText)); 347 | } else { 348 | return cb(new Error("Error communicating with server")); 349 | } 350 | }; 351 | req.onerror = function() { 352 | return cb(new Error("Error communicating with server")); 353 | }; 354 | return req.send(); 355 | }; 356 | })(this); 357 | _gatherData = function(perPage, currentPage, filter, sortDir, sortField) { 358 | var data; 359 | data = { 360 | perPage: perPage, 361 | page: currentPage 362 | }; 363 | if ((filter != null) && filter !== '') { 364 | data.filter = filter; 365 | } 366 | if ((sortDir != null) && sortDir !== '' && (sortField != null) && sortField !== '') { 367 | data.sortDir = sortDir; 368 | data.sortBy = sortField; 369 | } 370 | return data; 371 | }; 372 | this.filtering = ko.observable(false); 373 | this.pagedRows = ko.observableArray([]); 374 | this.numFilteredRows = ko.observable(0); 375 | this.filter.subscribe((function(_this) { 376 | return function() { 377 | return _this.currentPage(1); 378 | }; 379 | })(this)); 380 | this.perPage.subscribe((function(_this) { 381 | return function() { 382 | return _this.currentPage(1); 383 | }; 384 | })(this)); 385 | ko.computed((function(_this) { 386 | return function() { 387 | var data; 388 | _this.loading(true); 389 | _this.filtering(true); 390 | data = _gatherData(_this.perPage(), _this.currentPage(), _this.filter(), _this.sortDir(), _this.sortField()); 391 | return _getDataFromServer(data, function(err, response) { 392 | var results, total; 393 | _this.loading(false); 394 | _this.filtering(false); 395 | if (err) { 396 | return console.log(err); 397 | } 398 | total = response.total, results = response.results; 399 | _this.numFilteredRows(total); 400 | return _this.pagedRows(results.map(_this.options.resultHandlerFn)); 401 | }); 402 | }; 403 | })(this)).extend({ 404 | rateLimit: 500, 405 | method: 'notifyWhenChangesStop' 406 | }); 407 | this.pages = pureComputed((function(_this) { 408 | return function() { 409 | return Math.ceil(_this.numFilteredRows() / _this.perPage()); 410 | }; 411 | })(this)); 412 | this.leftPagerClass = pureComputed((function(_this) { 413 | return function() { 414 | if (_this.currentPage() === 1) { 415 | return 'disabled'; 416 | } 417 | }; 418 | })(this)); 419 | this.rightPagerClass = pureComputed((function(_this) { 420 | return function() { 421 | if (_this.currentPage() === _this.pages()) { 422 | return 'disabled'; 423 | } 424 | }; 425 | })(this)); 426 | this.from = pureComputed((function(_this) { 427 | return function() { 428 | return (_this.currentPage() - 1) * _this.perPage() + 1; 429 | }; 430 | })(this)); 431 | this.to = pureComputed((function(_this) { 432 | return function() { 433 | var to, total; 434 | to = _this.currentPage() * _this.perPage(); 435 | if (to > (total = _this.numFilteredRows())) { 436 | return total; 437 | } else { 438 | return to; 439 | } 440 | }; 441 | })(this)); 442 | this.recordsText = pureComputed((function(_this) { 443 | return function() { 444 | var from, pages, recordWord, recordWordPlural, to, total; 445 | pages = _this.pages(); 446 | total = _this.numFilteredRows(); 447 | from = _this.from(); 448 | to = _this.to(); 449 | recordWord = _this.options.recordWord; 450 | recordWordPlural = _this.options.recordWordPlural || recordWord + 's'; 451 | if (pages > 1) { 452 | return from + " to " + to + " of " + total + " " + recordWordPlural; 453 | } else { 454 | return total + " " + (total > 1 || total === 0 ? recordWordPlural : recordWord); 455 | } 456 | }; 457 | })(this)); 458 | this.showNoData = pureComputed((function(_this) { 459 | return function() { 460 | return _this.pagedRows().length === 0 && !_this.loading(); 461 | }; 462 | })(this)); 463 | this.showLoading = pureComputed((function(_this) { 464 | return function() { 465 | return _this.loading(); 466 | }; 467 | })(this)); 468 | this.sortClass = (function(_this) { 469 | return function(column) { 470 | return pureComputed(function() { 471 | if (_this.sortField() === column) { 472 | return 'sorted ' + (_this.sortDir() === 'asc' ? _this.options.ascSortClass : _this.options.descSortClass); 473 | } else { 474 | return _this.options.unsortedClass; 475 | } 476 | }); 477 | }; 478 | })(this); 479 | this.addRecord = function() { 480 | throw new Error("#addRecord() not applicable with serverSidePagination enabled"); 481 | }; 482 | this.removeRecord = function() { 483 | throw new Error("#removeRecord() not applicable with serverSidePagination enabled"); 484 | }; 485 | this.replaceRows = function() { 486 | throw new Error("#replaceRows() not applicable with serverSidePagination enabled"); 487 | }; 488 | return this.refreshData = (function(_this) { 489 | return function() { 490 | var data; 491 | _this.loading(true); 492 | _this.filtering(true); 493 | data = _gatherData(_this.perPage(), _this.currentPage(), _this.filter(), _this.sortDir(), _this.sortField()); 494 | return _getDataFromServer(data, function(err, response) { 495 | var results, total; 496 | _this.loading(false); 497 | _this.filtering(false); 498 | if (err) { 499 | return console.log(err); 500 | } 501 | total = response.total, results = response.results; 502 | _this.numFilteredRows(total); 503 | return _this.pagedRows(results.map(_this.options.resultHandlerFn)); 504 | }); 505 | }; 506 | })(this); 507 | }; 508 | 509 | DataTable.prototype.toggleSort = function(field) { 510 | return (function(_this) { 511 | return function() { 512 | _this.currentPage(1); 513 | if (_this.sortField() === field) { 514 | return _this.sortDir(_this.sortDir() === 'asc' ? 'desc' : 'asc'); 515 | } else { 516 | _this.sortDir(_this.options.sortDir); 517 | return _this.sortField(field); 518 | } 519 | }; 520 | })(this); 521 | }; 522 | 523 | DataTable.prototype.prevPage = function() { 524 | var page; 525 | page = this.currentPage(); 526 | if (page !== 1) { 527 | return this.currentPage(page - 1); 528 | } 529 | }; 530 | 531 | DataTable.prototype.nextPage = function() { 532 | var page; 533 | page = this.currentPage(); 534 | if (page !== this.pages()) { 535 | return this.currentPage(page + 1); 536 | } 537 | }; 538 | 539 | DataTable.prototype.gotoPage = function(page) { 540 | return (function(_this) { 541 | return function() { 542 | return _this.currentPage(page); 543 | }; 544 | })(this); 545 | }; 546 | 547 | DataTable.prototype.pageClass = function(page) { 548 | return pureComputed((function(_this) { 549 | return function() { 550 | if (_this.currentPage() === page) { 551 | return 'active'; 552 | } 553 | }; 554 | })(this)); 555 | }; 556 | 557 | return DataTable; 558 | 559 | })(); 560 | 561 | }).call(this); 562 | -------------------------------------------------------------------------------- /knockout-datatable.less: -------------------------------------------------------------------------------- 1 | .no-select { 2 | -webkit-touch-callout: none; 3 | -webkit-user-select: none; 4 | -khtml-user-select: none; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | } 9 | 10 | table { 11 | thead { 12 | tr { 13 | th.sortable { 14 | cursor: pointer; 15 | position: relative; 16 | padding-right: 12px; 17 | .no-select; 18 | i { 19 | color: #C5C5C5; 20 | position: absolute; 21 | top: 12px; 22 | right: 2px; 23 | &.sorted { 24 | color: black; 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /knockout-datatable.min.js: -------------------------------------------------------------------------------- 1 | (function(){var a=[].indexOf||function(a){for(var b=0,c=this.length;b=0||a===b)},b.prototype.initObservables=function(){return this.sortDir=ko.observable(this.options.sortDir),this.sortField=ko.observable(this.options.sortField),this.perPage=ko.observable(this.options.perPage),this.currentPage=ko.observable(1),this.filter=ko.observable(""),this.loading=ko.observable(!1),this.rows=ko.observableArray([])},b.prototype.initWithClientSidePagination=function(b){var e;return this.filtering=ko.observable(!1),this.forceFilter=ko.observable(!1),this.filter.subscribe(function(a){return function(){return a.currentPage(1)}}(this)),this.perPage.subscribe(function(a){return function(){return a.currentPage(1)}}(this)),this.rows(b),this.rowAttributeMap=d(function(a){return function(){var c,d,e;if(b=a.rows(),c={},b.length>0){e=b[0];for(d in e)e.hasOwnProperty(d)&&(c[d.toLowerCase()]=d)}return c}}(this)),this.filteredRows=d(function(a){return function(){var c,d;return a.filtering(!0),c=a.filter(),b=a.rows.slice(0),(a.forceFilter()||null!=c&&""!==c)&&(d=a.filterFn(c),b=b.filter(d)),null!=a.sortField()&&""!==a.sortField()&&b.sort(function(b,c){var d,e;return d=ko.utils.peekObservable(b[a.sortField()]),e=ko.utils.peekObservable(c[a.sortField()]),"string"==typeof d&&(d=d.toLowerCase()),"string"==typeof e&&(e=e.toLowerCase()),"asc"===a.sortDir()?de||""===e||null==e?1:0:de||""===e||null==e?-1:0}),a.filtering(!1),b}}(this)).extend({rateLimit:50,method:"notifyWhenChangesStop"}),this.pagedRows=d(function(a){return function(){var b,c;return b=a.currentPage()-1,c=a.perPage(),a.filteredRows().slice(b*c,(b+1)*c)}}(this)),this.pages=d(function(a){return function(){return Math.ceil(a.filteredRows().length/a.perPage())}}(this)),this.leftPagerClass=d(function(a){return function(){if(1===a.currentPage())return"disabled"}}(this)),this.rightPagerClass=d(function(a){return function(){if(a.currentPage()===a.pages())return"disabled"}}(this)),this.total=d(function(a){return function(){return a.filteredRows().length}}(this)),this.from=d(function(a){return function(){return(a.currentPage()-1)*a.perPage()+1}}(this)),this.to=d(function(a){return function(){var b;return b=a.currentPage()*a.perPage(),b>a.total()?a.total():b}}(this)),this.recordsText=d(function(a){return function(){var b,c,d,e,f,g;return c=a.pages(),g=a.total(),b=a.from(),f=a.to(),d=a.options.recordWord,e=a.options.recordWordPlural||d+"s",c>1?b+" to "+f+" of "+g+" "+e:g+" "+(g>1||0===g?e:d)}}(this)),this.showNoData=d(function(a){return function(){return 0===a.pagedRows().length&&!a.loading()}}(this)),this.showLoading=d(function(a){return function(){return a.loading()}}(this)),this.sortClass=function(a){return function(b){return d(function(){return a.sortField()===b?"sorted "+("asc"===a.sortDir()?a.options.ascSortClass:a.options.descSortClass):a.options.unsortedClass})}}(this),this.addRecord=function(a){return function(b){return a.rows.push(b)}}(this),this.removeRecord=function(a){return function(b){if(a.rows.remove(b),0===a.pagedRows().length)return a.prevPage()}}(this),this.replaceRows=function(a){return function(b){return a.rows(b),a.currentPage(1),a.filter(void 0)}}(this),e=function(a,b,d){var e,f;return function(){var a;a=[];for(e in d)f=d[e],a.push(f);return a}().some(function(d){return c(ko.isObservable(b[d])?b[d]():b[d],a)})},this.filterFn=this.options.filterFn||function(b){return function(d){var f,g,h,i;return g=null==d?"":d,h=[[],{}],f=h[0],i=h[1],g.split(" ").forEach(function(a){var b;return a.indexOf(":")>=0?(b=a.split(":"),i[b[0]]=function(){switch(b[1].toLowerCase()){case"yes":case"true":return!0;case"no":case"false":return!1;case"blank":case"none":case"null":case"undefined":return;default:return b[1].toLowerCase()}}()):f.push(a)}),f=f.join(" "),function(d){var g,h,j;return g=function(){var a;a=[];for(h in i)j=i[h],a.push(function(a){return function(b,e){var f;return!!(f=a.rowAttributeMap()[b.toLowerCase()])&&c(ko.isObservable(d[f])?d[f]():d[f],e)}}(this)(h,j));return a}.call(b),a.call(g,!1)<0&&(null!=d.match?d.match(f):e(f,d,b.rowAttributeMap()))}}}(this)},b.prototype.initWithServerSidePagination=function(){var a,b;return b=function(a){return function(b,c){var d,e,f,g;return f=a.options.paginationPath+"?"+function(){var a;a=[];for(d in b)g=b[d],a.push(encodeURIComponent(d)+"="+encodeURIComponent(g));return a}().join("&"),e=new XMLHttpRequest,e.open("GET",f,!0),e.setRequestHeader("Content-Type","application/json"),e.onload=function(){return e.status>=200&&e.status<400?c(null,JSON.parse(e.responseText)):c(new Error("Error communicating with server"))},e.onerror=function(){return c(new Error("Error communicating with server"))},e.send()}}(this),a=function(a,b,c,d,e){var f;return f={perPage:a,page:b},null!=c&&""!==c&&(f.filter=c),null!=d&&""!==d&&null!=e&&""!==e&&(f.sortDir=d,f.sortBy=e),f},this.filtering=ko.observable(!1),this.pagedRows=ko.observableArray([]),this.numFilteredRows=ko.observable(0),this.filter.subscribe(function(a){return function(){return a.currentPage(1)}}(this)),this.perPage.subscribe(function(a){return function(){return a.currentPage(1)}}(this)),ko.computed(function(c){return function(){var d;return c.loading(!0),c.filtering(!0),d=a(c.perPage(),c.currentPage(),c.filter(),c.sortDir(),c.sortField()),b(d,function(a,b){var d,e;return c.loading(!1),c.filtering(!1),a?console.log(a):(e=b.total,d=b.results,c.numFilteredRows(e),c.pagedRows(d.map(c.options.resultHandlerFn)))})}}(this)).extend({rateLimit:500,method:"notifyWhenChangesStop"}),this.pages=d(function(a){return function(){return Math.ceil(a.numFilteredRows()/a.perPage())}}(this)),this.leftPagerClass=d(function(a){return function(){if(1===a.currentPage())return"disabled"}}(this)),this.rightPagerClass=d(function(a){return function(){if(a.currentPage()===a.pages())return"disabled"}}(this)),this.from=d(function(a){return function(){return(a.currentPage()-1)*a.perPage()+1}}(this)),this.to=d(function(a){return function(){var b,c;return b=a.currentPage()*a.perPage(),b>(c=a.numFilteredRows())?c:b}}(this)),this.recordsText=d(function(a){return function(){var b,c,d,e,f,g;return c=a.pages(),g=a.numFilteredRows(),b=a.from(),f=a.to(),d=a.options.recordWord,e=a.options.recordWordPlural||d+"s",c>1?b+" to "+f+" of "+g+" "+e:g+" "+(g>1||0===g?e:d)}}(this)),this.showNoData=d(function(a){return function(){return 0===a.pagedRows().length&&!a.loading()}}(this)),this.showLoading=d(function(a){return function(){return a.loading()}}(this)),this.sortClass=function(a){return function(b){return d(function(){return a.sortField()===b?"sorted "+("asc"===a.sortDir()?a.options.ascSortClass:a.options.descSortClass):a.options.unsortedClass})}}(this),this.addRecord=function(){throw new Error("#addRecord() not applicable with serverSidePagination enabled")},this.removeRecord=function(){throw new Error("#removeRecord() not applicable with serverSidePagination enabled")},this.replaceRows=function(){throw new Error("#replaceRows() not applicable with serverSidePagination enabled")},this.refreshData=function(c){return function(){var d;return c.loading(!0),c.filtering(!0),d=a(c.perPage(),c.currentPage(),c.filter(),c.sortDir(),c.sortField()),b(d,function(a,b){var d,e;return c.loading(!1),c.filtering(!1),a?console.log(a):(e=b.total,d=b.results,c.numFilteredRows(e),c.pagedRows(d.map(c.options.resultHandlerFn)))})}}(this)},b.prototype.toggleSort=function(a){return function(b){return function(){return b.currentPage(1),b.sortField()===a?b.sortDir("asc"===b.sortDir()?"desc":"asc"):(b.sortDir(b.options.sortDir),b.sortField(a))}}(this)},b.prototype.prevPage=function(){var a;if(1!==(a=this.currentPage()))return this.currentPage(a-1)},b.prototype.nextPage=function(){var a;if((a=this.currentPage())!==this.pages())return this.currentPage(a+1)},b.prototype.gotoPage=function(a){return function(b){return function(){return b.currentPage(a)}}(this)},b.prototype.pageClass=function(a){return d(function(b){return function(){if(b.currentPage()===a)return"active"}}(this))},b}()}).call(this); 2 | //# sourceMappingURL=knockout-datatable.min.js.map -------------------------------------------------------------------------------- /knockout-datatable.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["knockout-datatable.js"],"names":["indexOf","item","i","l","this","length","window","DataTable","rows","options","serverSideOpts","Array","recordWord","recordWordPlural","sortDir","sortField","perPage","filterFn","unsortedClass","descSortClass","ascSortClass","initObservables","serverSidePagination","enabled","path","loader","Error","paginationPath","resultHandlerFn","initWithServerSidePagination","initWithClientSidePagination","primitiveCompare","pureComputed","ko","computed","item1","item2","toString","toLowerCase","prototype","observable","currentPage","filter","loading","observableArray","_defaultMatch","filtering","forceFilter","subscribe","_this","rowAttributeMap","attrMap","key","row","hasOwnProperty","filteredRows","slice","sort","a","b","aVal","bVal","utils","peekObservable","extend","rateLimit","method","pagedRows","pageIndex","pages","Math","ceil","leftPagerClass","rightPagerClass","total","from","to","recordsText","showNoData","showLoading","sortClass","column","addRecord","record","push","removeRecord","remove","prevPage","replaceRows","val","results1","some","isObservable","filter_text","filterVar","ref","specials","split","forEach","word","words","join","conditionals","rowAttr","call","match","_gatherData","_getDataFromServer","data","cb","req","url","encodeURIComponent","XMLHttpRequest","open","setRequestHeader","onload","status","JSON","parse","responseText","onerror","send","page","sortBy","numFilteredRows","err","response","results","console","log","map","refreshData","toggleSort","field","nextPage","gotoPage","pageClass"],"mappings":"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","file":"knockout-datatable.min.js"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockout-datatable", 3 | "version": "0.7.2", 4 | "private": false, 5 | "scripts": { 6 | "compile": "grunt" 7 | }, 8 | "devDependencies": { 9 | "chai": "~1.10.0", 10 | "grunt": "*", 11 | "grunt-contrib-coffee": "*", 12 | "grunt-contrib-less": "*", 13 | "grunt-contrib-uglify": "*", 14 | "grunt-karma": "~0.10.1", 15 | "karma": "~0.12.31", 16 | "karma-chai-sinon": "~0.1.4", 17 | "karma-chrome-launcher": "~0.1.7", 18 | "karma-firefox-launcher": "~0.1.4", 19 | "karma-mocha": "~0.1.10", 20 | "karma-mocha-reporter": "~0.3.1", 21 | "karma-phantomjs-launcher": "~0.1.4", 22 | "mocha": "~2.1.0", 23 | "sinon": "~1.12.2", 24 | "sinon-chai": "~2.6.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/client-side-pagination.js: -------------------------------------------------------------------------------- 1 | describe('DataTable', function(){ 2 | 3 | var view; 4 | 5 | describe("client-side pagination", function(){ 6 | 7 | it('should initialize with rows as first parameter', function(){ 8 | view = new DataTable([{foo: 'bar'}, {bar: 'baz'}], { 9 | recordWord: 'city', 10 | recordWordPlural: 'cities', 11 | sortDir: 'desc', 12 | sortField: 'population', 13 | perPage: 15 14 | }); 15 | assert.lengthOf(view.rows(), 2); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/knockout-datatable.js: -------------------------------------------------------------------------------- 1 | // these tests will be run in a browser via Karma.js 2 | 3 | describe('DataTable', function(){ 4 | var view; 5 | 6 | describe('construction', function(){ 7 | it('should accept options as first parameter', function(){ 8 | var opts = { 9 | recordWord: 'community', 10 | recordWordPlural: 'communities', 11 | sortDir: 'asc', 12 | sortField: 'name', 13 | perPage: 50, 14 | unsortedClass: 'sort-unsorted', 15 | descSortClass: 'sort-desc', 16 | ascSortClass: 'sort-asc' 17 | }; 18 | view = new DataTable(opts); 19 | }); 20 | 21 | it('should return instance of DataTable', function(){ 22 | assert.instanceOf(view, DataTable); 23 | }); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /test/server-side-pagination.js: -------------------------------------------------------------------------------- 1 | describe('DataTable', function(){ 2 | var view; 3 | 4 | describe('server-side pagination', function(){ 5 | var server; 6 | 7 | var _examplePaginationResponseFromServer = function(perPage, page, opts){ 8 | if (!opts) opts = {}; 9 | var total = opts.total || 100; 10 | if (!(perPage && page)) { 11 | throw new Error("perPage and page required to construct example response"); 12 | return; 13 | } 14 | var results = []; 15 | for (var i = ((page - 1) * perPage + 1); i <= Math.min(total, page * perPage); i++){ 16 | results.push({ 17 | id: i, 18 | name: 'res' + i 19 | }); 20 | } 21 | return JSON.stringify({ 22 | total: total, 23 | results: results 24 | }); 25 | }; 26 | 27 | // Waits 501ms (since datatable sends request 500ms after 28 | // changes have stopped) and triggers a response from the mock server 29 | var _waitAndServerRespond = function(perPage, page, opts, cb){ 30 | setTimeout(function(){ 31 | req = server.requests[0] 32 | req.respond(200, { 33 | "Content-Type": "application/json" 34 | }, _examplePaginationResponseFromServer(13, 1, opts)); 35 | server.restore(); 36 | server = sinon.fakeServer.create(); 37 | cb(req); 38 | }, 501); 39 | } 40 | 41 | beforeEach(function(){ 42 | server = sinon.fakeServer.create(); 43 | }); 44 | 45 | afterEach(function(){ 46 | server.restore() 47 | }); 48 | 49 | describe('construction', function(){ 50 | it('should throw error if missing loader', function(){ 51 | assert.throws(function(){ 52 | new DataTable({ 53 | perPage: 13, 54 | serverSidePagination: { 55 | enabled: true, 56 | path: '/api/communitites' 57 | } 58 | }) 59 | }); 60 | }); 61 | 62 | it('should throw error if missing path', function(){ 63 | assert.throws(function(){ 64 | new DataTable({ 65 | perPage: 13, 66 | serverSidePagination: { 67 | enabled: true, 68 | loader: function(result){return result;} 69 | } 70 | }) 71 | }); 72 | }); 73 | 74 | it('should get initial results', function(done){ 75 | assert.doesNotThrow(function(){ 76 | view = new DataTable({ 77 | perPage: 13, 78 | serverSidePagination: { 79 | enabled: true, 80 | path: '/api/communities', 81 | loader: function(result){ 82 | 83 | // attach a flag to test that the loader is being used 84 | result.type = 'foobar'; 85 | return result; 86 | } 87 | } 88 | }); 89 | }); 90 | _waitAndServerRespond(13, 1, {}, function(request){ 91 | var decodedURI = window.decodeURI(request.url); 92 | assert.include(decodedURI, '/api/communities?'); 93 | assert.include(decodedURI, 'perPage=13'); 94 | assert.include(decodedURI, 'page=1'); 95 | assert.notInclude(decodedURI, 'sortBy='); 96 | assert.notInclude(decodedURI, 'sortDir='); 97 | assert.notInclude(decodedURI, 'filter='); 98 | done(); 99 | }); 100 | }); 101 | }); 102 | describe('#pagedRows()', function(){ 103 | describe('should return the correct results', function(){ 104 | 105 | it('should return correct number of results', function(){ 106 | assert.lengthOf(view.pagedRows(), 13); 107 | }); 108 | 109 | it('should map results using `loader` function', function(){ 110 | var rows = view.pagedRows(); 111 | assert.equal(rows[0].name, 'res1'); 112 | assert.equal(rows[0].type, 'foobar'); 113 | }); 114 | }); 115 | }); 116 | describe('#nextPage()', function(){ 117 | it('should submit request for next page', function(done){ 118 | view.nextPage(); 119 | _waitAndServerRespond(13, 2, {}, function(request){ 120 | var decodedURI = window.decodeURI(request.url); 121 | assert.include(decodedURI, '/api/communities?'); 122 | assert.include(decodedURI, 'perPage=13'); 123 | assert.include(decodedURI, 'page=2'); 124 | assert.notInclude(decodedURI, 'sortBy='); 125 | assert.notInclude(decodedURI, 'sortDir='); 126 | assert.notInclude(decodedURI, 'filter='); 127 | done(); 128 | }); 129 | }); 130 | }); 131 | describe('#prevPage()', function(){ 132 | it('should submit request for previous page', function(done){ 133 | view.prevPage(); 134 | _waitAndServerRespond(13, 1, {}, function(request){ 135 | var decodedURI = window.decodeURI(request.url); 136 | assert.include(decodedURI, '/api/communities?'); 137 | assert.include(decodedURI, 'perPage=13'); 138 | assert.include(decodedURI, 'page=1'); 139 | assert.notInclude(decodedURI, 'sortBy='); 140 | assert.notInclude(decodedURI, 'sortDir='); 141 | assert.notInclude(decodedURI, 'filter='); 142 | done() 143 | }); 144 | }); 145 | }); 146 | describe('#toggleSort(fieldName)()', function(){ 147 | it('should submit request for current page, sorted desc', function(done){ 148 | view.toggleSort('name')(); 149 | _waitAndServerRespond(13, 1, {}, function(request){ 150 | var decodedURI = window.decodeURI(request.url); 151 | assert.include(decodedURI, '/api/communities?'); 152 | assert.include(decodedURI, 'perPage=13'); 153 | assert.include(decodedURI, 'page=1'); 154 | assert.include(decodedURI, 'sortBy=name'); 155 | assert.include(decodedURI, 'sortDir=asc'); 156 | assert.notInclude(decodedURI, 'filter='); 157 | done(); 158 | }); 159 | }); 160 | 161 | it('should submit request for current page, sorted asc', function(done){ 162 | view.toggleSort('name')(); 163 | _waitAndServerRespond(13, 1, {}, function(request){ 164 | var decodedURI = window.decodeURI(request.url); 165 | assert.include(decodedURI, '/api/communities?'); 166 | assert.include(decodedURI, 'perPage=13'); 167 | assert.include(decodedURI, 'page=1'); 168 | assert.include(decodedURI, 'sortBy=name'); 169 | assert.include(decodedURI, 'sortDir=desc'); 170 | assert.notInclude(decodedURI, 'filter='); 171 | done(); 172 | }); 173 | }); 174 | }); 175 | 176 | describe('#filter(filterText)', function(){ 177 | it('should submit request for current page, with filter', function(done){ 178 | view.filter('foo bar baz'); 179 | _waitAndServerRespond(13, 1, {total: 5}, function(request){ 180 | var decodedURI = window.decodeURI(request.url); 181 | assert.include(decodedURI, '/api/communities?'); 182 | assert.include(decodedURI, 'perPage=13'); 183 | assert.include(decodedURI, 'page=1'); 184 | assert.include(decodedURI, 'sortBy=name'); 185 | assert.include(decodedURI, 'sortDir=desc'); 186 | assert.include(decodedURI, 'filter=foo bar baz'); 187 | done(); 188 | }); 189 | }); 190 | }); 191 | 192 | describe('#gotoPage(pageNum)()', function(){ 193 | it('should submit request with page = pageNum', function(done){ 194 | view.filter(''); 195 | view.toggleSort('')(); 196 | _waitAndServerRespond(13, 1, {}, function(request){ 197 | view.gotoPage(3)(); 198 | _waitAndServerRespond(13, 3, {}, function(request){ 199 | var decodedURI = window.decodeURI(request.url); 200 | assert.include(decodedURI, '/api/communities?'); 201 | assert.include(decodedURI, 'perPage=13'); 202 | assert.include(decodedURI, 'page=3'); 203 | done(); 204 | }); 205 | }); 206 | }); 207 | 208 | it('should do something specific when pageNum is out of range?'); 209 | }); 210 | 211 | describe('#pages()', function(){ 212 | it('should be correct number of pages determined by response from server', function(done){ 213 | view.filter(''); 214 | view.toggleSort('')(); 215 | view.gotoPage(1)(); 216 | _waitAndServerRespond(13, 1, {}, function(request){ 217 | // from mock server: {total: 100, results: [...]} 218 | // with perPage of 13 and total of 100, should get (100 / 13).ceil 219 | assert.equal(view.pages(), Math.ceil(100 / 13)); 220 | done() 221 | }); 222 | }); 223 | 224 | it('should change when a request returns that server has a different total', function(done){ 225 | view.nextPage(); 226 | _waitAndServerRespond(13, 2, {total: 120}, function(request){ 227 | assert.equal(view.pages(), Math.ceil(120 / 13)); 228 | done(); 229 | }); 230 | }); 231 | }); 232 | 233 | describe('#pageClass(pageNum)()', function(){ 234 | 235 | it("should be undefined for pages that aren't the current page", function(done){ 236 | view.gotoPage(1)(); 237 | _waitAndServerRespond(13, 1, {}, function(request){ 238 | assert.equal(view.pageClass(2)(), undefined); 239 | assert.equal(view.pageClass(3)(), undefined); 240 | assert.equal(view.pageClass(20)(), undefined); 241 | done() 242 | }); 243 | }); 244 | 245 | it("should be active for current page", function(done){ 246 | assert.equal(view.pageClass(1)(), 'active'); 247 | view.gotoPage(2)(); 248 | _waitAndServerRespond(13, 2, {}, function(request){ 249 | assert.equal(view.pageClass(2)(), 'active'); 250 | assert.equal(view.pageClass(1)(), undefined); 251 | done(); 252 | }); 253 | }); 254 | }); 255 | 256 | describe('#refreshData()', function(){ 257 | it('should submit request with current state of view model', function(done){ 258 | view.perPage(13); 259 | view.gotoPage(1)(); 260 | view.filter(''); 261 | view.toggleSort('')(); 262 | _waitAndServerRespond(13, 1, {}, function(request){ 263 | assert.equal(server.requests.length, 0); 264 | view.refreshData(); 265 | assert.equal(server.requests.length, 1); 266 | 267 | var decodedURI = window.decodeURI(server.requests[0].url); 268 | assert.include(decodedURI, '/api/communities?'); 269 | assert.include(decodedURI, 'perPage=13'); 270 | assert.include(decodedURI, 'page=1'); 271 | assert.notInclude(decodedURI, 'sortBy='); 272 | assert.notInclude(decodedURI, 'sortDir='); 273 | assert.notInclude(decodedURI, 'filter='); 274 | 275 | server.requests[0].respond(200, { 276 | "Content-Type": "application/json" 277 | }, _examplePaginationResponseFromServer(13, 1)); 278 | done(); 279 | }); 280 | }); 281 | }); 282 | 283 | describe('#addRecord(newRecord)', function(){ 284 | it('should throw error if called', function(){ 285 | assert.throws(function(){ 286 | view.addRecord({any: 'thing'}); 287 | }, '#addRecord() not applicable with serverSidePagination enabled'); 288 | }); 289 | }); 290 | 291 | describe('#removeRecord(record)', function(){ 292 | it('should throw error if called', function(){ 293 | assert.throws(function(){ 294 | view.removeRecord({any: 'thing'}); 295 | }, '#removeRecord() not applicable with serverSidePagination enabled'); 296 | }); 297 | }); 298 | 299 | describe('#replaceRows(array)', function(){ 300 | it('should throw error if called', function(){ 301 | assert.throws(function(){ 302 | view.replaceRows([{any: 'thing'}]); 303 | }, '#replaceRows() not applicable with serverSidePagination enabled'); 304 | }); 305 | }); 306 | }); 307 | }); 308 | --------------------------------------------------------------------------------