├── .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 |
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="#">«</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="#">»</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 |
316 | City
317 |
318 |
319 |
320 | Country
321 |
322 |
323 |
324 | Population
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 | This table has no data.
333 |
334 |
335 |
336 |
337 |
338 | Loading data...
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
365 |
366 |
367 |
372 |
373 |
374 |
Example without sorting
375 |
376 |
377 | Results per page
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 | City
386 |
387 |
388 | Country
389 |
390 |
391 | Population
392 |
393 |
394 |
395 |
396 |
397 |
398 | This table has no data.
399 |
400 |
401 |
402 |
403 |
404 | Loading data...
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
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 |
--------------------------------------------------------------------------------