├── .gitignore ├── examples ├── leaderboard │ ├── .meteor │ │ ├── cordova-plugins │ │ ├── .gitignore │ │ ├── release │ │ ├── platforms │ │ ├── identifier │ │ ├── .id │ │ ├── packages │ │ ├── .finished-upgraders │ │ └── versions │ ├── leaderboard.html │ ├── leaderboard.css │ └── leaderboard.js ├── saved-state │ ├── .meteor │ │ ├── cordova-plugins │ │ ├── .gitignore │ │ ├── release │ │ ├── platforms │ │ ├── identifier │ │ ├── .id │ │ ├── packages │ │ ├── .finished-upgraders │ │ └── versions │ ├── leaderboard.html │ ├── leaderboard.css │ └── leaderboard.js ├── server │ ├── .meteor │ │ ├── .gitignore │ │ ├── release │ │ ├── platforms │ │ ├── .id │ │ ├── .finished-upgraders │ │ ├── packages │ │ └── versions │ ├── table.html │ └── table.js ├── table-features │ ├── .meteor │ │ ├── cordova-plugins │ │ ├── .gitignore │ │ ├── release │ │ ├── platforms │ │ ├── packages │ │ ├── .id │ │ ├── .finished-upgraders │ │ └── versions │ ├── table-features.css │ ├── table-features.html │ └── table-features.js └── advanced-filters │ ├── .meteor │ ├── .gitignore │ ├── release │ ├── platforms │ ├── .id │ ├── .finished-upgraders │ ├── packages │ └── versions │ ├── clear_filters_button.html │ ├── autocomplete_filter.html │ ├── date_filter.html │ ├── quote_filter.html │ ├── clear_filters_button.js │ ├── greater_than_filter.html │ ├── checkbox_filter.html │ ├── select_filter.js │ ├── compound_filter.html │ ├── select_filter.html │ ├── quote_filter.js │ ├── greater_than_filter.js │ ├── date_filter.js │ ├── checkbox_filter.js │ ├── compound_filter.js │ ├── autocomplete_filter.js │ ├── leaderboard.css │ ├── leaderboard.html │ └── leaderboard.js ├── test ├── test_no_data_template.html ├── test_events_tmpl.html ├── README.md ├── test_reactivity.html ├── test_template.html ├── test_fields_tmpl.html ├── test_events.js ├── test_template.js ├── test_reactivity_server.js ├── test_i18n.js ├── test_filtering_server.js ├── helpers.js ├── test_compatibility.js ├── test_collection_argument.js ├── test_multiple_tables.js ├── test_pagination.js ├── test_column_toggles.js ├── test_reactivity.js ├── test_filtering.js ├── test_sorting.js └── test_settings.js ├── LICENSE ├── lib ├── filter.html ├── reactive_table.css ├── server.js ├── sort.js ├── reactive_table.html ├── reactive_table_i18n.js └── filter.js ├── versions.json └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /examples/leaderboard/.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/saved-state/.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/server/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/server/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.1 2 | -------------------------------------------------------------------------------- /examples/table-features/.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/leaderboard/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/leaderboard/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.1 2 | -------------------------------------------------------------------------------- /examples/saved-state/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/saved-state/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.1 2 | -------------------------------------------------------------------------------- /examples/table-features/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/advanced-filters/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/advanced-filters/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.1 2 | -------------------------------------------------------------------------------- /examples/server/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/table-features/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.1 2 | -------------------------------------------------------------------------------- /examples/leaderboard/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/saved-state/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/advanced-filters/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/leaderboard/.meteor/identifier: -------------------------------------------------------------------------------- 1 | 1aqojff1qds7wz1aca7ne 2 | -------------------------------------------------------------------------------- /examples/saved-state/.meteor/identifier: -------------------------------------------------------------------------------- 1 | 1aqojff1qds7wz1aca7ne 2 | -------------------------------------------------------------------------------- /examples/table-features/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /test/test_no_data_template.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_events_tmpl.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/table-features/table-features.css: -------------------------------------------------------------------------------- 1 | #content { 2 | margin: 100px; 3 | } 4 | 5 | #table { 6 | margin-top: 3em; 7 | } 8 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | Start the test app: 2 | 3 | meteor test-packages ./ 4 | 5 | And open http://localhost:3000 to run the tests 6 | -------------------------------------------------------------------------------- /test/test_reactivity.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_template.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /examples/advanced-filters/clear_filters_button.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_fields_tmpl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /examples/advanced-filters/autocomplete_filter.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /examples/server/table.html: -------------------------------------------------------------------------------- 1 | 2 | Reactive Table 3 | 4 | 5 | 6 |

Welcome to Reactive Table!

7 | 8 | {{> table}} 9 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /examples/advanced-filters/date_filter.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/advanced-filters/quote_filter.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/advanced-filters/clear_filters_button.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | 3 | Template.clearFiltersButton.events({ 4 | "click #clear-filters-button" : function () { 5 | ReactiveTable.clearFilters(['filter1', 'filter2', 'filter3', 'date-filter', 'checkbox-filter']); 6 | } 7 | }); 8 | 9 | } -------------------------------------------------------------------------------- /examples/advanced-filters/greater_than_filter.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/server/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 134onf5cggsx7x68ee7 8 | -------------------------------------------------------------------------------- /examples/table-features/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | standard-app-packages 7 | autopublish 8 | twbs:bootstrap-noglyph 9 | aslagle:reactive-table 10 | standard-minifier-css 11 | standard-minifier-js 12 | -------------------------------------------------------------------------------- /examples/leaderboard/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | l394661aru71g1gfqsy5 8 | -------------------------------------------------------------------------------- /examples/saved-state/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | l394661aru71g1gfqsy5 8 | -------------------------------------------------------------------------------- /examples/advanced-filters/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | no5plc1767uzekceye8 8 | -------------------------------------------------------------------------------- /examples/advanced-filters/checkbox_filter.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/table-features/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | l7em6qaz16yt17typqu 8 | -------------------------------------------------------------------------------- /examples/advanced-filters/select_filter.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | Template.selectFilter.created = function () { 3 | this.filter = new ReactiveTable.Filter('select-filter', ['score']); 4 | }; 5 | 6 | Template.selectFilter.events({ 7 | "change .select-filter": function (event, template) { 8 | template.filter.set($(event.target).val()); 9 | } 10 | }); 11 | } -------------------------------------------------------------------------------- /examples/advanced-filters/compound_filter.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/leaderboard/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | standard-app-packages 7 | autopublish 8 | insecure 9 | aslagle:reactive-table 10 | anti:i18n 11 | twbs:bootstrap-noglyph 12 | fortawesome:fontawesome 13 | standard-minifier-css 14 | standard-minifier-js 15 | -------------------------------------------------------------------------------- /examples/saved-state/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | standard-app-packages 7 | autopublish 8 | insecure 9 | aslagle:reactive-table 10 | anti:i18n 11 | twbs:bootstrap-noglyph 12 | fortawesome:fontawesome 13 | reactive-var 14 | standard-minifier-css 15 | standard-minifier-js 16 | -------------------------------------------------------------------------------- /examples/server/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /examples/leaderboard/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /examples/saved-state/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /examples/advanced-filters/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /examples/table-features/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /examples/advanced-filters/select_filter.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/advanced-filters/quote_filter.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | Template.quoteFilter.created = function () { 3 | this.filter = new ReactiveTable.Filter('quote-filter'); 4 | }; 5 | 6 | Template.quoteFilter.events({ 7 | "keyup .quote-filter-input, input .quote-filter-input": function (event, template) { 8 | var input = $(event.target).val(); 9 | if (input) { 10 | template.filter.set('"' + input + '"'); 11 | } else { 12 | template.filter.set(""); 13 | } 14 | } 15 | }); 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Amy Slagle 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /examples/server/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | insecure 8 | aslagle:reactive-table 9 | twbs:bootstrap 10 | audit-argument-checks 11 | meteor-base 12 | mobile-experience 13 | mongo 14 | blaze-html-templates 15 | session 16 | jquery 17 | tracker 18 | logging 19 | reload 20 | random 21 | ejson 22 | spacebars 23 | check 24 | standard-minifier-css 25 | standard-minifier-js 26 | -------------------------------------------------------------------------------- /examples/advanced-filters/greater_than_filter.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | Template.greaterThanFilter.created = function () { 3 | this.filter = new ReactiveTable.Filter('greater-than-filter', ['score']); 4 | }; 5 | 6 | Template.greaterThanFilter.events({ 7 | "keyup .greater-than-filter-input, input .greater-than-filter-input": function (event, template) { 8 | var input = parseInt($(event.target).val(), 10); 9 | if (!_.isNaN(input)) { 10 | template.filter.set({'$gt': input}); 11 | } else { 12 | template.filter.set(""); 13 | } 14 | } 15 | }); 16 | } -------------------------------------------------------------------------------- /examples/advanced-filters/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | autopublish 8 | insecure 9 | aslagle:reactive-table 10 | twbs:bootstrap 11 | momentjs:moment 12 | linto:jquery-ui 13 | jquery 14 | mizzao:autocomplete 15 | meteor-base 16 | mobile-experience 17 | mongo 18 | blaze-html-templates 19 | session 20 | tracker 21 | logging 22 | reload 23 | random 24 | ejson 25 | spacebars 26 | check 27 | standard-minifier-css 28 | standard-minifier-js 29 | -------------------------------------------------------------------------------- /examples/advanced-filters/date_filter.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | Template.dateFilter.created = function () { 3 | this.filter = new ReactiveTable.Filter('date-filter', ['date']); 4 | }; 5 | 6 | Template.dateFilter.rendered = function () { 7 | $('.date-selector').datepicker(); 8 | }; 9 | 10 | Template.dateFilter.events({ 11 | "change .date-selector": function (event, template) { 12 | if ($(event.target).val()) { 13 | var date = moment($(event.target).val()); 14 | template.filter.set(date.format("YYYY-MM-DD")); 15 | } else { 16 | template.filter.set(""); 17 | } 18 | } 19 | }); 20 | } -------------------------------------------------------------------------------- /examples/server/table.js: -------------------------------------------------------------------------------- 1 | var Rows = new Meteor.Collection('rows'); 2 | 3 | if (Meteor.isClient) { 4 | 5 | Template.table.helpers({ 6 | fields: function () { 7 | return ['string', 'number']; 8 | } 9 | }); 10 | } 11 | 12 | if (Meteor.isServer) { 13 | Meteor.startup(function () { 14 | if (Rows.find().count() === 0) { 15 | for (var i = 0; i < 20000; i++) { 16 | var randomString = Math.random().toString(36).substr(7); 17 | var randomNumber = Math.floor(Math.random() * 100); 18 | Rows.insert({string: randomString, number: randomNumber}); 19 | } 20 | } 21 | }); 22 | 23 | ReactiveTable.publish('rows', function () { return Rows; }, {}); 24 | } 25 | -------------------------------------------------------------------------------- /test/test_events.js: -------------------------------------------------------------------------------- 1 | testAsyncMulti('Events - click event', [function (test, expect) { 2 | var expectClicked = expect(function () { 3 | Blaze.remove(table); 4 | }); 5 | 6 | var clickHandler = function () { 7 | test.equal(this.name, "Ada Lovelace", "this should be the row object"); 8 | test.equal(this.score, 5, "this should be the row object"); 9 | expectClicked(); 10 | }; 11 | 12 | Template.testEventsTmpl.events({ 13 | "click .reactive-table tbody tr": clickHandler 14 | }); 15 | 16 | var table = Blaze.renderWithData( 17 | Template.testEventsTmpl, 18 | {collection: rows}, 19 | document.body 20 | ); 21 | 22 | $('.reactive-table tbody tr:first-child').click(); 23 | }]); 24 | -------------------------------------------------------------------------------- /examples/advanced-filters/checkbox_filter.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | Template.checkboxFilter.created = function () { 3 | this.filter = new ReactiveTable.Filter('checkbox-filter', ['checked']); 4 | }; 5 | 6 | Template.checkboxFilter.helpers({ 7 | checked: function () { 8 | if (Template.instance().filter.get() === "true") { 9 | return "checked"; 10 | } 11 | return ""; 12 | } 13 | }); 14 | 15 | Template.checkboxFilter.events({ 16 | "change .checkbox-filter": function (event, template) { 17 | if ($(event.target).is(":checked")) { 18 | template.filter.set("true"); 19 | } else { 20 | template.filter.set(""); 21 | } 22 | } 23 | }); 24 | } -------------------------------------------------------------------------------- /test/test_template.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('Template - nested', function (test) { 2 | var view = Blaze.renderWithData( 3 | Template.testTmpl, 4 | {collection: rows, array: [1]}, 5 | document.body 6 | ); 7 | test.length($('.reactive-table th'), 2, "two columns should be rendered"); 8 | test.length($('.reactive-table tbody tr td:first-child'), 6, "six rows should be rendered and have cells"); 9 | Blaze.remove(view); 10 | 11 | var view2 = Blaze.renderWithData( 12 | Template.testTmpl, 13 | {collection: rows, array: [{fields: 'abc'}]}, 14 | document.body 15 | ); 16 | test.length($('.reactive-table th'), 2, "two columns should be rendered"); 17 | test.length($('.reactive-table tbody tr td:first-child'), 6, "six rows should be rendered and have cells"); 18 | Blaze.remove(view2); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/filter.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /examples/leaderboard/leaderboard.html: -------------------------------------------------------------------------------- 1 | 2 | Leaderboard 3 | 4 | 5 | 6 |
7 | {{> leaderboard}} 8 |
9 | 10 | 11 | 30 | 31 | 37 | -------------------------------------------------------------------------------- /examples/saved-state/leaderboard.html: -------------------------------------------------------------------------------- 1 | 2 | Leaderboard 3 | 4 | 5 | 6 |
7 | {{> leaderboard}} 8 |
9 | 10 | 11 | 30 | 31 | 37 | -------------------------------------------------------------------------------- /test/test_reactivity_server.js: -------------------------------------------------------------------------------- 1 | var collection = new Mongo.Collection('reactivity-test'); 2 | collection.remove({}); 3 | collection.insert({name: 'item 1', value: 1}); 4 | 5 | ReactiveTable.publish('reactivity-test', collection); 6 | 7 | Meteor.methods({ 8 | "testInsert": function () { 9 | collection.insert({name: 'item 2', value: 2}); 10 | }, 11 | 12 | "testRemove": function () { 13 | collection.remove({name: 'item 2'}); 14 | }, 15 | 16 | "testUpdate": function () { 17 | collection.update({name: 'item 1'}, {'$set': {value: 2}}); 18 | } 19 | }); 20 | 21 | var collection2 = new Mongo.Collection('reactivity-test-access'); 22 | collection2.remove({}); 23 | collection2.insert({name: 'item 1', value: 1}); 24 | 25 | if (!Meteor.users.findOne()) { 26 | Accounts.createUser({username: 'abcd', password: 'abcd1234'}); 27 | } 28 | 29 | 30 | ReactiveTable.publish('reactivity-test-access', function () { 31 | if (this.userId) { 32 | return collection2; 33 | } else { 34 | return []; 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /examples/advanced-filters/compound_filter.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | Template.compoundFilter.created = function () { 3 | this.checkFilter = new ReactiveTable.Filter('compound-check-filter', ['checked']); 4 | this.scoreFilter = new ReactiveTable.Filter('compound-score-filter', ['score']); 5 | }; 6 | 7 | Template.compoundFilter.helpers({ 8 | checked: function () { 9 | var checkTrue = Template.instance().checkFilter.get() === "true"; 10 | var scoreTrue = _.isObject(Template.instance().scoreFilter.get()); 11 | if (checkTrue && scoreTrue) { 12 | return "checked"; 13 | } 14 | return ""; 15 | } 16 | }); 17 | 18 | Template.compoundFilter.events({ 19 | "change .compound-checkbox": function (event, template) { 20 | if ($(event.target).is(":checked")) { 21 | template.checkFilter.set("true"); 22 | template.scoreFilter.set({'$gt': 20}); 23 | } else { 24 | template.checkFilter.set(""); 25 | template.scoreFilter.set(""); 26 | } 27 | } 28 | }); 29 | } -------------------------------------------------------------------------------- /test/test_i18n.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('i18n - default english', function (test) { 2 | testTable( 3 | rows, 4 | function () { 5 | test.equal($('.reactive-table-filter span').text().trim(), "Filter", "filter text"); 6 | test.length($('.reactive-table-navigation .rows-per-page label span:first-of-type').text().trim().match(/^Show$/), 1, "show text") 7 | test.length($('.reactive-table-navigation .rows-per-page .rows-per-page-label').text().trim().match(/^rows\sper\spage$/), 1, "rows per page text"); 8 | } 9 | ); 10 | }); 11 | 12 | Tinytest.add('i18n - french', function (test) { 13 | i18n.setLanguage('fr'); 14 | testTable( 15 | rows, 16 | function () { 17 | test.equal($('.reactive-table-filter span').text().trim(), "Filtre", "filter text"); 18 | test.length($('.reactive-table-navigation .rows-per-page label span:first-of-type').text().trim().match(/^Voir$/), 1, "show text") 19 | test.length($('.reactive-table-navigation .rows-per-page .rows-per-page-label').text().trim().match(/^lignes\spar\spage$/), 1, "rows per page text"); 20 | } 21 | ); 22 | i18n.setLanguage('en'); 23 | }); 24 | -------------------------------------------------------------------------------- /test/test_filtering_server.js: -------------------------------------------------------------------------------- 1 | var collection = new Mongo.Collection('filter-regex-test'); 2 | 3 | collection.remove({}); 4 | 5 | collection.insert({name: 'item 1', value: '1+2'}); 6 | collection.insert({name: 'item 2', value: 'abc'}); 7 | 8 | ReactiveTable.publish('filter-regex-disabled', collection, {}, {enableRegex: false}); 9 | ReactiveTable.publish('filter-regex-enabled', collection, {}, {enableRegex: true}); 10 | 11 | ReactiveTable.publish('filter-inclusion', collection, {}, {fields: {name: 1}}); 12 | ReactiveTable.publish('filter-exclusion', collection, {}, {fields: {value: 0}}); 13 | 14 | var collectionWithNestedKeys = new Mongo.Collection('filter-nested-keys'); 15 | 16 | collectionWithNestedKeys.remove({}); 17 | 18 | collectionWithNestedKeys.insert({'name': 'item 1', 'nested': [{'value': 'value 1'}, {'value': 'other'}]}); 19 | collectionWithNestedKeys.insert({'name': 'item 2', 'nested': [{'value': 'value 2'}]}); 20 | 21 | ReactiveTable.publish('nested-filter-inclusion', collectionWithNestedKeys, {}, {fields: {nested: 1}}); 22 | ReactiveTable.publish('nested-filter-exclusion', collectionWithNestedKeys, {}, {fields: {nested: 0}}); 23 | 24 | ReactiveTable.publish('nested-filter-inclusion-with-array', collectionWithNestedKeys, {}, {fields: {"nested.value": 1}}) -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | testTable = function(tableData, testFunction) { 2 | var table = Blaze.renderWithData( 3 | Template.reactiveTable, 4 | tableData, 5 | document.body 6 | ); 7 | testFunction(); 8 | Blaze.remove(table); 9 | }; 10 | 11 | rows = [ 12 | {name: 'Ada Lovelace', score: 5}, 13 | {name: 'Grace Hopper', score: 10}, 14 | {name: 'Marie Curie', score: 5}, 15 | {name: 'Carl Friedrich Gauss', score: 0}, 16 | {name: 'Nikola Tesla', score: 15}, 17 | {name: 'Claude Shannon', score: 5} 18 | ]; 19 | 20 | if (Meteor.isClient) { 21 | collection = new Mongo.Collection(); 22 | _.each(rows, function (row) { 23 | collection.insert(row); 24 | }); 25 | } 26 | 27 | if (Meteor.isServer) { 28 | collection = new Mongo.Collection('players'); 29 | collection.remove({}); 30 | _.each(rows, function (row) { 31 | collection.insert(row); 32 | }); 33 | 34 | ReactiveTable.publish('collection', collection); 35 | 36 | ReactiveTable.publish('collection-and-selector', collection, {score: 5}); 37 | 38 | ReactiveTable.publish('collection-function', function () { 39 | return collection; 40 | }); 41 | 42 | ReactiveTable.publish('selector-function', collection, function () { 43 | return {score: 5}; 44 | }); 45 | 46 | ReactiveTable.publish('collection-or-filter', collection, {}, {filterOperator: "$or"}); 47 | } 48 | -------------------------------------------------------------------------------- /examples/advanced-filters/autocomplete_filter.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | Template.autocompleteFilter.created = function () { 3 | this.filter = new ReactiveTable.Filter('autocomplete-filter', ['name']); 4 | }; 5 | 6 | Template.autocompleteFilter.helpers({ 7 | settings: function () { 8 | var filter = Template.instance().filter; 9 | return { 10 | position: "top", 11 | limit: 5, 12 | rules: [ 13 | { 14 | collection: "Players", 15 | subscription: "players-autocomplete", 16 | field: "name", 17 | template: Template.namePill, 18 | callback: function(doc) { 19 | return filter.set(doc.name); 20 | } 21 | } 22 | ] 23 | }; 24 | } 25 | }); 26 | 27 | Template.autocompleteFilter.events({ 28 | "input #name-autocomplete-filter": function (event, template) { 29 | var input = $(event.target).val(); 30 | if (input) { 31 | template.filter.set(input); 32 | } else { 33 | template.filter.set(""); 34 | } 35 | } 36 | }); 37 | } 38 | 39 | if (Meteor.isServer) { 40 | Meteor.publish("players-autocomplete", function (selector, options) { 41 | // guard against client-side DOS: hard limit to 50 42 | if (options.limit) { 43 | options.limit = Math.min(50, Math.abs(options.limit)); 44 | } 45 | 46 | Autocomplete.publishCursor(Players.find(selector, options), this); 47 | this.ready(); 48 | }) 49 | } -------------------------------------------------------------------------------- /examples/leaderboard/leaderboard.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 200; 4 | margin: 50px 0; 5 | padding: 0; 6 | -webkit-user-select: none; 7 | -khtml-user-select: none; 8 | -moz-user-select: none; 9 | -o-user-select: none; 10 | user-select: none; 11 | } 12 | 13 | .leaderboard { 14 | margin-top: 150px; 15 | border-top: 1px solid black; 16 | } 17 | 18 | #table { 19 | -webkit-user-select: text; 20 | -khtml-user-select: text; 21 | -moz-user-select: text; 22 | -o-user-select: text; 23 | user-select: text; 24 | } 25 | 26 | #outer { 27 | width: 600px; 28 | margin: 0 auto; 29 | } 30 | 31 | .player { 32 | cursor: pointer; 33 | padding: 5px; 34 | } 35 | 36 | .player .name { 37 | display: inline-block; 38 | width: 300px; 39 | font-size: 1.75em; 40 | } 41 | 42 | .player .score { 43 | display: inline-block; 44 | width: 100px; 45 | text-align: right; 46 | font-size: 2em; 47 | font-weight: bold; 48 | color: #777; 49 | } 50 | 51 | .player.selected { 52 | background-color: yellow; 53 | } 54 | 55 | .player.selected .score { 56 | color: black; 57 | } 58 | 59 | .details, .none { 60 | font-weight: bold; 61 | font-size: 2em; 62 | border-style: dashed none none none; 63 | border-color: #ccc; 64 | border-width: 4px; 65 | margin: 50px 10px; 66 | padding: 10px 0px; 67 | } 68 | 69 | .none { 70 | color: #777; 71 | } 72 | 73 | .inc { 74 | cursor: pointer; 75 | } 76 | -------------------------------------------------------------------------------- /examples/saved-state/leaderboard.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 200; 4 | margin: 50px 0; 5 | padding: 0; 6 | -webkit-user-select: none; 7 | -khtml-user-select: none; 8 | -moz-user-select: none; 9 | -o-user-select: none; 10 | user-select: none; 11 | } 12 | 13 | .leaderboard { 14 | margin-top: 150px; 15 | border-top: 1px solid black; 16 | } 17 | 18 | #table { 19 | -webkit-user-select: text; 20 | -khtml-user-select: text; 21 | -moz-user-select: text; 22 | -o-user-select: text; 23 | user-select: text; 24 | } 25 | 26 | #outer { 27 | width: 600px; 28 | margin: 0 auto; 29 | } 30 | 31 | .player { 32 | cursor: pointer; 33 | padding: 5px; 34 | } 35 | 36 | .player .name { 37 | display: inline-block; 38 | width: 300px; 39 | font-size: 1.75em; 40 | } 41 | 42 | .player .score { 43 | display: inline-block; 44 | width: 100px; 45 | text-align: right; 46 | font-size: 2em; 47 | font-weight: bold; 48 | color: #777; 49 | } 50 | 51 | .player.selected { 52 | background-color: yellow; 53 | } 54 | 55 | .player.selected .score { 56 | color: black; 57 | } 58 | 59 | .details, .none { 60 | font-weight: bold; 61 | font-size: 2em; 62 | border-style: dashed none none none; 63 | border-color: #ccc; 64 | border-width: 4px; 65 | margin: 50px 10px; 66 | padding: 10px 0px; 67 | } 68 | 69 | .none { 70 | color: #777; 71 | } 72 | 73 | .inc { 74 | cursor: pointer; 75 | } 76 | -------------------------------------------------------------------------------- /examples/table-features/.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.0.3 2 | anti:i18n@0.4.3 3 | aslagle:reactive-table@0.8.29 4 | autopublish@1.0.6 5 | autoupdate@1.2.7 6 | babel-compiler@6.6.1 7 | babel-runtime@0.1.7 8 | base64@1.0.7 9 | binary-heap@1.0.7 10 | blaze@2.1.6 11 | blaze-tools@1.0.7 12 | boilerplate-generator@1.0.7 13 | caching-compiler@1.0.3 14 | caching-html-compiler@1.0.5 15 | callback-hook@1.0.7 16 | check@1.1.3 17 | ddp@1.2.4 18 | ddp-client@1.2.4 19 | ddp-common@1.2.4 20 | ddp-server@1.2.5 21 | deps@1.0.11 22 | diff-sequence@1.0.4 23 | ecmascript@0.4.2 24 | ecmascript-runtime@0.2.9 25 | ejson@1.0.10 26 | fastclick@1.0.10 27 | geojson-utils@1.0.7 28 | html-tools@1.0.8 29 | htmljs@1.0.8 30 | http@1.1.4 31 | id-map@1.0.6 32 | jquery@1.11.7 33 | launch-screen@1.0.10 34 | livedata@1.0.17 35 | logging@1.0.11 36 | meteor@1.1.13 37 | meteor-platform@1.2.5 38 | minifier-css@1.1.10 39 | minifier-js@1.1.10 40 | minimongo@1.0.13 41 | mobile-status-bar@1.0.11 42 | modules@0.5.2 43 | modules-runtime@0.6.2 44 | mongo@1.1.6 45 | mongo-id@1.0.3 46 | npm-mongo@1.4.42 47 | observe-sequence@1.0.10 48 | ordered-dict@1.0.6 49 | promise@0.6.6 50 | random@1.0.8 51 | reactive-dict@1.1.6 52 | reactive-var@1.0.8 53 | reload@1.1.7 54 | retry@1.0.6 55 | routepolicy@1.0.9 56 | session@1.1.4 57 | spacebars@1.0.10 58 | spacebars-compiler@1.0.10 59 | standard-app-packages@1.0.8 60 | standard-minifier-css@1.0.5 61 | standard-minifier-js@1.0.5 62 | templating@1.1.8 63 | templating-tools@1.0.3 64 | tracker@1.0.12 65 | twbs:bootstrap-noglyph@3.3.5 66 | ui@1.0.10 67 | underscore@1.0.7 68 | url@1.0.8 69 | webapp@1.2.7 70 | webapp-hashing@1.0.8 71 | -------------------------------------------------------------------------------- /examples/advanced-filters/leaderboard.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 200; 4 | margin: 50px 0; 5 | padding: 0; 6 | -webkit-user-select: none; 7 | -khtml-user-select: none; 8 | -moz-user-select: none; 9 | -o-user-select: none; 10 | user-select: none; 11 | } 12 | 13 | .leaderboard { 14 | margin-top: 150px; 15 | border-top: 1px solid black; 16 | } 17 | 18 | #table { 19 | -webkit-user-select: text; 20 | -khtml-user-select: text; 21 | -moz-user-select: text; 22 | -o-user-select: text; 23 | user-select: text; 24 | } 25 | 26 | #outer { 27 | width: 600px; 28 | margin: 0 auto; 29 | } 30 | 31 | .player { 32 | cursor: pointer; 33 | padding: 5px; 34 | } 35 | 36 | .player .name { 37 | display: inline-block; 38 | width: 300px; 39 | font-size: 1.75em; 40 | } 41 | 42 | .player .score { 43 | display: inline-block; 44 | width: 100px; 45 | text-align: right; 46 | font-size: 2em; 47 | font-weight: bold; 48 | color: #777; 49 | } 50 | 51 | .player.selected { 52 | background-color: yellow; 53 | } 54 | 55 | .player.selected .score { 56 | color: black; 57 | } 58 | 59 | .details, .none { 60 | font-weight: bold; 61 | font-size: 2em; 62 | border-style: dashed none none none; 63 | border-color: #ccc; 64 | border-width: 4px; 65 | margin: 50px 10px; 66 | padding: 10px 0px; 67 | } 68 | 69 | .none { 70 | color: #777; 71 | } 72 | 73 | .inc { 74 | cursor: pointer; 75 | } 76 | 77 | #filter .input-group { 78 | margin: 5px 0px; 79 | } 80 | -------------------------------------------------------------------------------- /lib/reactive_table.css: -------------------------------------------------------------------------------- 1 | .reactive-table-options { 2 | padding-right: 0px; 3 | margin-right: -5px; 4 | } 5 | 6 | .reactive-table-filter { 7 | float: right; 8 | } 9 | 10 | .reactive-table-columns-dropdown { 11 | float: right; 12 | } 13 | 14 | .reactive-table-columns-dropdown button { 15 | float: right; 16 | } 17 | 18 | .reactive-table .sortable, .reactive-table-add-column { 19 | cursor: pointer; 20 | -webkit-user-select: none; 21 | -moz-user-select: none; 22 | -ms-user-select: none; 23 | user-select: none; 24 | } 25 | 26 | .table > thead > tr > th.reactive-table-add-column { 27 | border-bottom: none; 28 | } 29 | 30 | .reactive-table-navigation { 31 | display: inline-block; 32 | width: 100%; 33 | } 34 | 35 | .reactive-table-navigation .form-inline input { 36 | width: 45px; 37 | } 38 | 39 | .reactive-table-navigation .rows-per-page { 40 | float: left; 41 | } 42 | 43 | .reactive-table-navigation .page-number { 44 | float: right; 45 | } 46 | 47 | .reactive-table-navigation .previous-page, 48 | .reactive-table-navigation .next-page { 49 | font-size: 130%; 50 | margin: 0 5px; 51 | padding: 0.5em; 52 | } 53 | 54 | .reactive-table-navigation .previous-page.fa, 55 | .reactive-table-navigation .next-page.fa { 56 | vertical-align: middle; 57 | } 58 | 59 | .reactive-table-navigation .previous-page:hover, 60 | .reactive-table-navigation .next-page:hover { 61 | cursor: pointer; 62 | } 63 | 64 | .reactive-table .fa-sort-asc { 65 | position: relative; 66 | top: -2px; 67 | } 68 | 69 | .reactive-table .fa-sort-desc { 70 | position: relative; 71 | top: 2px; 72 | } 73 | -------------------------------------------------------------------------------- /examples/leaderboard/.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.0.3 2 | anti:i18n@0.4.3 3 | aslagle:reactive-table@0.8.29 4 | autopublish@1.0.6 5 | autoupdate@1.2.7 6 | babel-compiler@6.6.1 7 | babel-runtime@0.1.7 8 | base64@1.0.7 9 | binary-heap@1.0.7 10 | blaze@2.1.6 11 | blaze-tools@1.0.7 12 | boilerplate-generator@1.0.7 13 | caching-compiler@1.0.3 14 | caching-html-compiler@1.0.5 15 | callback-hook@1.0.7 16 | check@1.1.3 17 | ddp@1.2.4 18 | ddp-client@1.2.4 19 | ddp-common@1.2.4 20 | ddp-server@1.2.5 21 | deps@1.0.11 22 | diff-sequence@1.0.4 23 | ecmascript@0.4.2 24 | ecmascript-runtime@0.2.9 25 | ejson@1.0.10 26 | fastclick@1.0.10 27 | fortawesome:fontawesome@4.5.0 28 | geojson-utils@1.0.7 29 | html-tools@1.0.8 30 | htmljs@1.0.8 31 | http@1.1.4 32 | id-map@1.0.6 33 | insecure@1.0.6 34 | jquery@1.11.7 35 | launch-screen@1.0.10 36 | livedata@1.0.17 37 | logging@1.0.11 38 | meteor@1.1.13 39 | meteor-platform@1.2.5 40 | minifier-css@1.1.10 41 | minifier-js@1.1.10 42 | minimongo@1.0.13 43 | mobile-status-bar@1.0.11 44 | modules@0.5.2 45 | modules-runtime@0.6.2 46 | mongo@1.1.6 47 | mongo-id@1.0.3 48 | npm-mongo@1.4.42 49 | observe-sequence@1.0.10 50 | ordered-dict@1.0.6 51 | promise@0.6.6 52 | random@1.0.8 53 | reactive-dict@1.1.6 54 | reactive-var@1.0.8 55 | reload@1.1.7 56 | retry@1.0.6 57 | routepolicy@1.0.9 58 | session@1.1.4 59 | spacebars@1.0.10 60 | spacebars-compiler@1.0.10 61 | standard-app-packages@1.0.8 62 | standard-minifier-css@1.0.5 63 | standard-minifier-js@1.0.5 64 | templating@1.1.8 65 | templating-tools@1.0.3 66 | tracker@1.0.12 67 | twbs:bootstrap-noglyph@3.3.5 68 | ui@1.0.10 69 | underscore@1.0.7 70 | url@1.0.8 71 | webapp@1.2.7 72 | webapp-hashing@1.0.8 73 | -------------------------------------------------------------------------------- /examples/saved-state/.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.0.3 2 | anti:i18n@0.4.3 3 | aslagle:reactive-table@0.8.29 4 | autopublish@1.0.6 5 | autoupdate@1.2.7 6 | babel-compiler@6.6.1 7 | babel-runtime@0.1.7 8 | base64@1.0.7 9 | binary-heap@1.0.7 10 | blaze@2.1.6 11 | blaze-tools@1.0.7 12 | boilerplate-generator@1.0.7 13 | caching-compiler@1.0.3 14 | caching-html-compiler@1.0.5 15 | callback-hook@1.0.7 16 | check@1.1.3 17 | ddp@1.2.4 18 | ddp-client@1.2.4 19 | ddp-common@1.2.4 20 | ddp-server@1.2.5 21 | deps@1.0.11 22 | diff-sequence@1.0.4 23 | ecmascript@0.4.2 24 | ecmascript-runtime@0.2.9 25 | ejson@1.0.10 26 | fastclick@1.0.10 27 | fortawesome:fontawesome@4.5.0 28 | geojson-utils@1.0.7 29 | html-tools@1.0.8 30 | htmljs@1.0.8 31 | http@1.1.4 32 | id-map@1.0.6 33 | insecure@1.0.6 34 | jquery@1.11.7 35 | launch-screen@1.0.10 36 | livedata@1.0.17 37 | logging@1.0.11 38 | meteor@1.1.13 39 | meteor-platform@1.2.5 40 | minifier-css@1.1.10 41 | minifier-js@1.1.10 42 | minimongo@1.0.13 43 | mobile-status-bar@1.0.11 44 | modules@0.5.2 45 | modules-runtime@0.6.2 46 | mongo@1.1.6 47 | mongo-id@1.0.3 48 | npm-mongo@1.4.42 49 | observe-sequence@1.0.10 50 | ordered-dict@1.0.6 51 | promise@0.6.6 52 | random@1.0.8 53 | reactive-dict@1.1.6 54 | reactive-var@1.0.8 55 | reload@1.1.7 56 | retry@1.0.6 57 | routepolicy@1.0.9 58 | session@1.1.4 59 | spacebars@1.0.10 60 | spacebars-compiler@1.0.10 61 | standard-app-packages@1.0.8 62 | standard-minifier-css@1.0.5 63 | standard-minifier-js@1.0.5 64 | templating@1.1.8 65 | templating-tools@1.0.3 66 | tracker@1.0.12 67 | twbs:bootstrap-noglyph@3.3.5 68 | ui@1.0.10 69 | underscore@1.0.7 70 | url@1.0.8 71 | webapp@1.2.7 72 | webapp-hashing@1.0.8 73 | -------------------------------------------------------------------------------- /examples/advanced-filters/leaderboard.html: -------------------------------------------------------------------------------- 1 | 2 | Leaderboard 3 | 4 | 5 | 6 |
7 | {{> leaderboard}} 8 |
9 | 10 | 11 | 51 | 52 | 58 | -------------------------------------------------------------------------------- /examples/server/.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.0.3 2 | anti:i18n@0.4.3 3 | aslagle:reactive-table@0.8.29 4 | audit-argument-checks@1.0.6 5 | autoupdate@1.2.7 6 | babel-compiler@6.6.1 7 | babel-runtime@0.1.7 8 | base64@1.0.7 9 | binary-heap@1.0.7 10 | blaze@2.1.6 11 | blaze-html-templates@1.0.3 12 | blaze-tools@1.0.7 13 | boilerplate-generator@1.0.7 14 | caching-compiler@1.0.3 15 | caching-html-compiler@1.0.5 16 | callback-hook@1.0.7 17 | check@1.1.3 18 | ddp@1.2.4 19 | ddp-client@1.2.4 20 | ddp-common@1.2.4 21 | ddp-server@1.2.5 22 | deps@1.0.11 23 | diff-sequence@1.0.4 24 | ecmascript@0.4.2 25 | ecmascript-runtime@0.2.9 26 | ejson@1.0.10 27 | fastclick@1.0.10 28 | geojson-utils@1.0.7 29 | hot-code-push@1.0.3 30 | html-tools@1.0.8 31 | htmljs@1.0.8 32 | http@1.1.4 33 | id-map@1.0.6 34 | insecure@1.0.6 35 | jquery@1.11.7 36 | launch-screen@1.0.10 37 | livedata@1.0.17 38 | logging@1.0.11 39 | meteor@1.1.13 40 | meteor-base@1.0.3 41 | minifier-css@1.1.10 42 | minifier-js@1.1.10 43 | minimongo@1.0.13 44 | mobile-experience@1.0.3 45 | mobile-status-bar@1.0.11 46 | modules@0.5.2 47 | modules-runtime@0.6.2 48 | mongo@1.1.6 49 | mongo-id@1.0.3 50 | npm-mongo@1.4.42 51 | observe-sequence@1.0.10 52 | ordered-dict@1.0.6 53 | promise@0.6.6 54 | random@1.0.8 55 | reactive-dict@1.1.6 56 | reactive-var@1.0.8 57 | reload@1.1.7 58 | retry@1.0.6 59 | routepolicy@1.0.9 60 | session@1.1.4 61 | spacebars@1.0.10 62 | spacebars-compiler@1.0.10 63 | standard-minifier-css@1.0.5 64 | standard-minifier-js@1.0.5 65 | templating@1.1.8 66 | templating-tools@1.0.3 67 | tracker@1.0.12 68 | twbs:bootstrap@3.3.6 69 | ui@1.0.10 70 | underscore@1.0.7 71 | url@1.0.8 72 | webapp@1.2.7 73 | webapp-hashing@1.0.8 74 | -------------------------------------------------------------------------------- /examples/table-features/table-features.html: -------------------------------------------------------------------------------- 1 | 2 | JavaScript table components comparison 3 | 4 | 5 | 6 | {{> forkMe}} 7 |
8 |

JavaScript table/grid components comparison

9 | {{> about}} 10 | {{> featureComparison}} 11 | {{> footer}} 12 |
13 | 14 | 15 | 20 | 21 | 24 | 25 | 28 | 29 | 32 | -------------------------------------------------------------------------------- /examples/advanced-filters/.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.0.3 2 | anti:i18n@0.4.3 3 | aslagle:reactive-table@0.8.29 4 | autopublish@1.0.6 5 | autoupdate@1.2.7 6 | babel-compiler@6.6.1 7 | babel-runtime@0.1.7 8 | base64@1.0.7 9 | binary-heap@1.0.7 10 | blaze@2.1.6 11 | blaze-html-templates@1.0.3 12 | blaze-tools@1.0.7 13 | boilerplate-generator@1.0.7 14 | caching-compiler@1.0.3 15 | caching-html-compiler@1.0.5 16 | callback-hook@1.0.7 17 | check@1.1.3 18 | coffeescript@1.0.16 19 | dandv:caret-position@2.1.1 20 | ddp@1.2.4 21 | ddp-client@1.2.4 22 | ddp-common@1.2.4 23 | ddp-server@1.2.5 24 | deps@1.0.11 25 | diff-sequence@1.0.4 26 | ecmascript@0.4.2 27 | ecmascript-runtime@0.2.9 28 | ejson@1.0.10 29 | fastclick@1.0.10 30 | geojson-utils@1.0.7 31 | hot-code-push@1.0.3 32 | html-tools@1.0.8 33 | htmljs@1.0.8 34 | http@1.1.4 35 | id-map@1.0.6 36 | insecure@1.0.6 37 | jquery@1.11.7 38 | launch-screen@1.0.10 39 | linto:jquery-ui@1.11.2 40 | livedata@1.0.17 41 | logging@1.0.11 42 | meteor@1.1.13 43 | meteor-base@1.0.3 44 | minifier-css@1.1.10 45 | minifier-js@1.1.10 46 | minimongo@1.0.13 47 | mizzao:autocomplete@0.5.1 48 | mobile-experience@1.0.3 49 | mobile-status-bar@1.0.11 50 | modules@0.5.2 51 | modules-runtime@0.6.2 52 | momentjs:moment@2.12.0 53 | mongo@1.1.6 54 | mongo-id@1.0.3 55 | npm-mongo@1.4.42 56 | observe-sequence@1.0.10 57 | ordered-dict@1.0.6 58 | promise@0.6.6 59 | random@1.0.8 60 | reactive-dict@1.1.6 61 | reactive-var@1.0.8 62 | reload@1.1.7 63 | retry@1.0.6 64 | routepolicy@1.0.9 65 | session@1.1.4 66 | spacebars@1.0.10 67 | spacebars-compiler@1.0.10 68 | standard-minifier-css@1.0.5 69 | standard-minifier-js@1.0.5 70 | templating@1.1.8 71 | templating-tools@1.0.3 72 | tracker@1.0.12 73 | twbs:bootstrap@3.3.6 74 | ui@1.0.10 75 | underscore@1.0.7 76 | url@1.0.8 77 | webapp@1.2.7 78 | webapp-hashing@1.0.8 79 | -------------------------------------------------------------------------------- /examples/leaderboard/leaderboard.js: -------------------------------------------------------------------------------- 1 | // Set up a collection to contain player information. On the server, 2 | // it is backed by a MongoDB collection named "players". 3 | 4 | Players = new Meteor.Collection("players"); 5 | 6 | if (Meteor.isClient) { 7 | Template.leaderboard.helpers({ 8 | players : function () { 9 | return Players.find({}, {sort: {score: -1, name: 1}}); 10 | }, 11 | 12 | tableSettings : function () { 13 | return { 14 | fields: [ 15 | { key: 'name', label: 'Full Name' }, 16 | { key: 'name', label: 'First Name', fn: function (name) { return name ? name.split(' ')[0] : ''; } }, 17 | { key: 'score', label: 'Score' } 18 | ] 19 | }; 20 | }, 21 | 22 | selected_name : function () { 23 | var player = Players.findOne(Session.get("selected_player")); 24 | return player && player.name; 25 | } 26 | }); 27 | 28 | Template.player.helpers({ 29 | selected : function () { 30 | return Session.equals("selected_player", this._id) ? "selected" : ''; 31 | } 32 | }); 33 | 34 | Template.leaderboard.events({ 35 | 'click input.inc': function () { 36 | Players.update(Session.get("selected_player"), {$inc: {score: 5}}); 37 | } 38 | }); 39 | 40 | Template.player.events({ 41 | 'click': function () { 42 | Session.set("selected_player", this._id); 43 | } 44 | }); 45 | } 46 | 47 | // On server startup, create some players if the database is empty. 48 | if (Meteor.isServer) { 49 | Meteor.startup(function () { 50 | if (Players.find().count() === 0) { 51 | var names = ["Ada Lovelace", 52 | "Grace Hopper", 53 | "Marie Curie", 54 | "Carl Friedrich Gauss", 55 | "Nikola Tesla", 56 | "Claude Shannon"]; 57 | for (var i = 0; i < names.length; i++) 58 | Players.insert({name: names[i], score: Math.floor(Random.fraction()*10)*5}); 59 | } 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "anti:i18n", 5 | "0.4.3" 6 | ], 7 | [ 8 | "application-configuration", 9 | "1.0.3" 10 | ], 11 | [ 12 | "base64", 13 | "1.0.1" 14 | ], 15 | [ 16 | "binary-heap", 17 | "1.0.1" 18 | ], 19 | [ 20 | "blaze", 21 | "2.0.3" 22 | ], 23 | [ 24 | "callback-hook", 25 | "1.0.1" 26 | ], 27 | [ 28 | "check", 29 | "1.0.2" 30 | ], 31 | [ 32 | "ddp", 33 | "1.0.11" 34 | ], 35 | [ 36 | "deps", 37 | "1.0.5" 38 | ], 39 | [ 40 | "ejson", 41 | "1.0.4" 42 | ], 43 | [ 44 | "follower-livedata", 45 | "1.0.2" 46 | ], 47 | [ 48 | "geojson-utils", 49 | "1.0.1" 50 | ], 51 | [ 52 | "htmljs", 53 | "1.0.2" 54 | ], 55 | [ 56 | "id-map", 57 | "1.0.1" 58 | ], 59 | [ 60 | "jquery", 61 | "1.0.1" 62 | ], 63 | [ 64 | "json", 65 | "1.0.1" 66 | ], 67 | [ 68 | "logging", 69 | "1.0.5" 70 | ], 71 | [ 72 | "meteor", 73 | "1.1.3" 74 | ], 75 | [ 76 | "minimongo", 77 | "1.0.5" 78 | ], 79 | [ 80 | "mongo", 81 | "1.0.8" 82 | ], 83 | [ 84 | "observe-sequence", 85 | "1.0.3" 86 | ], 87 | [ 88 | "ordered-dict", 89 | "1.0.1" 90 | ], 91 | [ 92 | "random", 93 | "1.0.1" 94 | ], 95 | [ 96 | "reactive-var", 97 | "1.0.3" 98 | ], 99 | [ 100 | "retry", 101 | "1.0.1" 102 | ], 103 | [ 104 | "templating", 105 | "1.0.9" 106 | ], 107 | [ 108 | "tracker", 109 | "1.0.3" 110 | ], 111 | [ 112 | "ui", 113 | "1.0.4" 114 | ], 115 | [ 116 | "underscore", 117 | "1.0.1" 118 | ] 119 | ], 120 | "pluginDependencies": [], 121 | "toolVersion": "meteor-tool@1.0.35", 122 | "format": "1.0" 123 | } -------------------------------------------------------------------------------- /examples/saved-state/leaderboard.js: -------------------------------------------------------------------------------- 1 | // Set up a collection to contain player information. On the server, 2 | // it is backed by a MongoDB collection named "players". 3 | 4 | Players = new Meteor.Collection("players"); 5 | 6 | if (Meteor.isClient) { 7 | Template.leaderboard.onCreated(function () { 8 | var currentPage = new ReactiveVar(Session.get('current-page') || 0); 9 | this.currentPage = currentPage; 10 | this.autorun(function () { 11 | Session.set('current-page', currentPage.get()); 12 | }); 13 | }); 14 | 15 | Template.leaderboard.helpers({ 16 | players : function () { 17 | return Players.find({}, {sort: {score: -1, name: 1}}); 18 | }, 19 | 20 | tableSettings : function () { 21 | return { 22 | currentPage: Template.instance().currentPage, 23 | fields: [ 24 | { key: 'name', label: 'Full Name' }, 25 | { key: 'name', label: 'First Name', fn: function (name) { return name ? name.split(' ')[0] : ''; } }, 26 | { key: 'score', label: 'Score' } 27 | ] 28 | }; 29 | }, 30 | 31 | selected_name : function () { 32 | var player = Players.findOne(Session.get("selected_player")); 33 | return player && player.name; 34 | } 35 | }); 36 | 37 | Template.player.helpers({ 38 | selected : function () { 39 | return Session.equals("selected_player", this._id) ? "selected" : ''; 40 | } 41 | }); 42 | 43 | Template.leaderboard.events({ 44 | 'click input.inc': function () { 45 | Players.update(Session.get("selected_player"), {$inc: {score: 5}}); 46 | } 47 | }); 48 | 49 | Template.player.events({ 50 | 'click': function () { 51 | Session.set("selected_player", this._id); 52 | } 53 | }); 54 | } 55 | 56 | // On server startup, create some players if the database is empty. 57 | if (Meteor.isServer) { 58 | Meteor.startup(function () { 59 | if (Players.find().count() === 0) { 60 | var names = ["Ada Lovelace", 61 | "Grace Hopper", 62 | "Marie Curie", 63 | "Carl Friedrich Gauss", 64 | "Nikola Tesla", 65 | "Claude Shannon"]; 66 | for (var i = 0; i < names.length; i++) 67 | Players.insert({name: names[i], score: Math.floor(Random.fraction()*10)*5}); 68 | } 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /test/test_compatibility.js: -------------------------------------------------------------------------------- 1 | testAsyncMulti('Compatibility - collection with dburles:collection-helpers', [function (test, expect) { 2 | var collectionWithHelpers = new Mongo.Collection(); 3 | _.each(rows, function (row) { 4 | collectionWithHelpers.insert(row); 5 | }); 6 | collectionWithHelpers.helpers({ 7 | "first": function () { 8 | return this.name.slice(0, 1); 9 | } 10 | }); 11 | 12 | var table = Blaze.renderWithData( 13 | Template.reactiveTable, 14 | { 15 | collection: collectionWithHelpers, 16 | fields: [ 17 | {key: "first", label: "first", fn: function (val, obj) { return obj.first(); } } 18 | ] 19 | }, 20 | document.body 21 | ); 22 | test.length($('.reactive-table tbody tr'), 6, "rendered table should have 6 rows"); 23 | test.equal($('.reactive-table tbody tr:first-child td').text(), "A", "table should display output of helper"); 24 | 25 | collectionWithHelpers.update({name: 'Ada Lovelace'}, {'$set': {name: 'Bbbbb'}}); 26 | Meteor.setTimeout(expect(function () { 27 | test.equal($('.reactive-table tbody tr:first-child td').text(), "B", "table should display output of helper"); 28 | Blaze.remove(table); 29 | }), 0); 30 | }]); 31 | 32 | testAsyncMulti('Compatibility - cursor with dburles:collection-helpers', [function (test, expect) { 33 | var collectionWithHelpers = new Mongo.Collection(); 34 | _.each(rows, function (row) { 35 | collectionWithHelpers.insert(row); 36 | }); 37 | collectionWithHelpers.helpers({ 38 | "first": function () { 39 | return this.name.slice(0, 1); 40 | } 41 | }); 42 | 43 | var table = Blaze.renderWithData( 44 | Template.reactiveTable, 45 | { 46 | collection: collectionWithHelpers.find(), 47 | fields: [ 48 | {key: "first", label: "first", fn: function (val, obj) { return obj.first(); } } 49 | ] 50 | }, 51 | document.body 52 | ); 53 | test.length($('.reactive-table tbody tr'), 6, "rendered table should have 6 rows"); 54 | test.equal($('.reactive-table tbody tr:first-child td').text(), "A", "table should display output of helper"); 55 | 56 | collectionWithHelpers.update({name: 'Ada Lovelace'}, {'$set': {name: 'Bbbbb'}}); 57 | Meteor.setTimeout(expect(function () { 58 | test.equal($('.reactive-table tbody tr:first-child td').text(), "B", "table should display output of helper"); 59 | Blaze.remove(table); 60 | }), 0); 61 | }]); 62 | -------------------------------------------------------------------------------- /examples/advanced-filters/leaderboard.js: -------------------------------------------------------------------------------- 1 | // Set up a collection to contain player information. On the server, 2 | // it is backed by a MongoDB collection named "players". 3 | 4 | Players = new Meteor.Collection("players"); 5 | 6 | if (Meteor.isClient) { 7 | Template.leaderboard.helpers({ 8 | players : function () { 9 | return Players.find({}, {sort: {score: -1, name: 1}}); 10 | }, 11 | 12 | tableSettings : function () { 13 | return { 14 | fields: [ 15 | { key: 'name', label: 'Full Name' }, 16 | { key: 'name', label: 'First Name', fn: function (name) { return name ? name.split(' ')[0] : ''; } }, 17 | { key: 'score', label: 'Score' }, 18 | { key: 'date', label: 'Date', sortByValue: true, fn: function (date) { return moment(date).format("dddd, MMMM Do YYYY"); }}, 19 | { 20 | key: 'checked', 21 | label: 'Checked', 22 | fn: function (checked) { 23 | var html = '' 24 | if (checked === false) { 25 | html = ''; 26 | } 27 | return new Spacebars.SafeString(html); 28 | } 29 | } 30 | ], 31 | filters: [ 32 | 'filter1', 33 | 'filter2', 34 | 'filter3', 35 | 'date-filter', 36 | 'checkbox-filter', 37 | 'greater-than-filter', 38 | 'compound-check-filter', 39 | 'compound-score-filter', 40 | 'quote-filter', 41 | 'autocomplete-filter', 42 | 'select-filter' 43 | ] 44 | }; 45 | }, 46 | 47 | nameFields: function () { 48 | return ['name']; 49 | }, 50 | 51 | scoreFields: function () { 52 | return ['score']; 53 | }, 54 | 55 | selected_name : function () { 56 | var player = Players.findOne(Session.get("selected_player")); 57 | return player && player.name; 58 | } 59 | }); 60 | 61 | Template.player.helpers({ 62 | selected : function () { 63 | return Session.equals("selected_player", this._id) ? "selected" : ''; 64 | } 65 | }); 66 | 67 | Template.leaderboard.events({ 68 | 'click input.inc': function () { 69 | Players.update(Session.get("selected_player"), {$inc: {score: 5}}); 70 | } 71 | }); 72 | 73 | Template.player.events({ 74 | 'click': function () { 75 | Session.set("selected_player", this._id); 76 | } 77 | }); 78 | } 79 | 80 | // On server startup, create some players if the database is empty. 81 | if (Meteor.isServer) { 82 | Meteor.startup(function () { 83 | if (Players.find().count() === 0) { 84 | var names = ["Ada Lovelace", 85 | "Grace Hopper", 86 | "Marie Curie", 87 | "Carl Friedrich Gauss", 88 | "Nikola Tesla", 89 | "Claude Shannon"]; 90 | var dates = [ 91 | "2014-12-10", 92 | "2011-12-09", 93 | "2016-11-07", 94 | "2012-04-30", 95 | "2014-07-10", 96 | "2016-04-30" 97 | ]; 98 | for (var i = 0; i < names.length; i++) 99 | Players.insert({name: names[i], score: Math.floor(Random.fraction()*10)*5, date: dates[i], checked: (Random.fraction()>0.5)}); 100 | } 101 | }); 102 | 103 | ReactiveTable.publish('players', Players); 104 | } 105 | -------------------------------------------------------------------------------- /test/test_collection_argument.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('Collection Argument - array named argument', function (test) { 2 | testTable( 3 | {collection: rows}, 4 | function () { 5 | test.length($('.reactive-table tbody tr'), 6, "rendered table should have 6 rows"); 6 | } 7 | ); 8 | }); 9 | 10 | Tinytest.add('Collection Argument - collection named argument', function (test) { 11 | testTable( 12 | {collection: collection}, 13 | function () { 14 | test.length($('.reactive-table tbody tr'), 6, "rendered table should have 6 rows"); 15 | } 16 | ); 17 | }); 18 | 19 | Tinytest.add('Collection Argument - cursor named argument', function (test) { 20 | testTable( 21 | {collection: collection.find({score: 5})}, 22 | function () { 23 | test.length($('.reactive-table tbody tr'), 3, "rendered table should have 3 rows"); 24 | } 25 | ); 26 | }); 27 | 28 | Tinytest.add('Collection Argument - collection as only argument', function (test) { 29 | testTable( 30 | collection, 31 | function () { 32 | test.length($('.reactive-table tbody tr'), 6, "rendered table should have 6 rows"); 33 | } 34 | ); 35 | }); 36 | 37 | Tinytest.add('Collection Argument - in settings argument', function (test) { 38 | testTable( 39 | {settings: {collection: rows}}, 40 | function () { 41 | test.length($('.reactive-table tbody tr'), 6, "rendered table should have 6 rows"); 42 | } 43 | ); 44 | }); 45 | 46 | testAsyncMulti('Collection Argument - server-side collection', [function (test, expect) { 47 | var table = Blaze.renderWithData( 48 | Template.reactiveTable, 49 | {collection: 'collection', fields: ['name', 'score']}, 50 | document.body 51 | ); 52 | 53 | var expectSixRows = expect(function () { 54 | test.length($('.reactive-table tbody tr'), 6, "rendered table should have 6 rows"); 55 | Blaze.remove(table); 56 | }); 57 | 58 | Meteor.setTimeout(expectSixRows, 500); 59 | }]); 60 | 61 | testAsyncMulti('Collection Argument - server-side collection with selector', [function (test, expect) { 62 | var table = Blaze.renderWithData( 63 | Template.reactiveTable, 64 | {collection: 'collection-and-selector', fields: ['name', 'score']}, 65 | document.body 66 | ); 67 | 68 | var expectThreeRows = expect(function () { 69 | test.length($('.reactive-table tbody tr'), 3, "rendered table should have 3 rows"); 70 | Blaze.remove(table); 71 | }); 72 | 73 | Meteor.setTimeout(expectThreeRows, 500); 74 | }]); 75 | 76 | testAsyncMulti('Collection Argument - server-side collection function', [function (test, expect) { 77 | var table = Blaze.renderWithData( 78 | Template.reactiveTable, 79 | {collection: 'collection-function', fields: ['name', 'score']}, 80 | document.body 81 | ); 82 | 83 | var expectSixRows = expect(function () { 84 | test.length($('.reactive-table tbody tr'), 6, "rendered table should have 6 rows"); 85 | Blaze.remove(table); 86 | }); 87 | 88 | Meteor.setTimeout(expectSixRows, 500); 89 | }]); 90 | 91 | testAsyncMulti('Collection Argument - server-side collection with selector function', [function (test, expect) { 92 | var table = Blaze.renderWithData( 93 | Template.reactiveTable, 94 | {collection: 'selector-function', fields: ['name', 'score']}, 95 | document.body 96 | ); 97 | 98 | var expectThreeRows = expect(function () { 99 | test.length($('.reactive-table tbody tr'), 3, "rendered table should have 3 rows"); 100 | Blaze.remove(table); 101 | }); 102 | 103 | Meteor.setTimeout(expectThreeRows, 500); 104 | }]); 105 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "A reactive table designed for Meteor", 3 | version: "0.8.45", 4 | name: "aslagle:reactive-table", 5 | git: "https://github.com/aslagle/reactive-table.git" 6 | }); 7 | 8 | Package.on_use(function (api) { 9 | api.versionsFrom("METEOR@0.9.0"); 10 | api.use('templating', 'client'); 11 | api.use('jquery', 'client'); 12 | api.use('underscore', ['server', 'client']); 13 | api.use('tracker@1.0.9', 'client'); 14 | api.use('reactive-var@1.0.3', 'client'); 15 | api.use("anti:i18n@0.4.3", 'client'); 16 | api.use("mongo@1.0.8", ["server", "client"]); 17 | api.use("check", "server"); 18 | 19 | api.use("fortawesome:fontawesome@4.2.0", 'client', {weak: true}); 20 | 21 | api.add_files('lib/reactive_table.html', 'client'); 22 | api.add_files('lib/filter.html', 'client'); 23 | api.add_files('lib/reactive_table_i18n.js', 'client'); 24 | api.add_files('lib/reactive_table.js', 'client'); 25 | api.add_files('lib/reactive_table.css', 'client'); 26 | api.add_files('lib/sort.js', 'client'); 27 | api.add_files('lib/filter.js', ['client', 'server']); 28 | api.add_files('lib/server.js', 'server'); 29 | 30 | api.export("ReactiveTable", ["client", "server"]); 31 | }); 32 | 33 | Package.on_test(function (api) { 34 | api.use('templating', 'client'); 35 | api.use('jquery', 'client'); 36 | api.use('underscore', ['client', 'server']); 37 | api.use('tracker@1.0.9', 'client'); 38 | api.use('reactive-var@1.0.3', 'client'); 39 | api.use("anti:i18n@0.4.3", 'client'); 40 | api.use("mongo", ["server", "client"]); 41 | api.use("check", "server"); 42 | api.use("audit-argument-checks", "server"); 43 | 44 | api.add_files('lib/reactive_table.html', 'client'); 45 | api.add_files('lib/filter.html', 'client'); 46 | api.add_files('lib/reactive_table_i18n.js', 'client'); 47 | api.add_files('lib/reactive_table.js', 'client'); 48 | api.add_files('lib/reactive_table.css', 'client'); 49 | api.add_files('lib/sort.js', 'client'); 50 | api.add_files('lib/filter.js', ['client', 'server']); 51 | api.add_files('lib/server.js', 'server'); 52 | 53 | api.export("ReactiveTable", ["client", "server"]); 54 | 55 | api.use(['tinytest', 'test-helpers'], 'client'); 56 | api.add_files('test/helpers.js', ['client', 'server']); 57 | api.add_files('test/test_collection_argument.js', 'client'); 58 | api.add_files('test/test_no_data_template.html', 'client'); 59 | api.add_files('test/test_settings.js', 'client'); 60 | api.add_files('test/test_fields_tmpl.html', 'client'); 61 | api.add_files('test/test_fields.js', 'client'); 62 | 63 | api.use('accounts-password@1.0.6', ['client', 'server']); 64 | api.add_files('test/test_reactivity_server.js', 'server'); 65 | api.add_files('test/test_reactivity.html', 'client'); 66 | api.add_files('test/test_reactivity.js', 'client'); 67 | 68 | api.add_files('test/test_sorting.js', 'client'); 69 | api.add_files('test/test_filtering_server.js', 'server'); 70 | api.add_files('test/test_filtering.js', 'client'); 71 | api.add_files('test/test_pagination.js', 'client'); 72 | api.add_files('test/test_i18n.js', 'client'); 73 | api.add_files('test/test_events_tmpl.html', 'client'); 74 | api.add_files('test/test_events.js', 'client'); 75 | api.add_files('test/test_column_toggles.js', 'client'); 76 | api.add_files('test/test_multiple_tables.js', 'client'); 77 | api.add_files('test/test_template.html', 'client'); 78 | api.add_files('test/test_template.js', 'client'); 79 | api.add_files('test/test_custom_filters.js', 'client'); 80 | 81 | api.use("dburles:collection-helpers@1.0.1", "client"); 82 | api.add_files("test/test_compatibility.js", "client"); 83 | }); 84 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | ReactiveTable = {}; 2 | 3 | ReactiveTable.publish = function (name, collectionOrFunction, selectorOrFunction, settings) { 4 | Meteor.publish("reactive-table-" + name, function (publicationId, filters, fields, options, rowsPerPage) { 5 | check(publicationId, String); 6 | check(filters, [Match.OneOf(String, Object, Mongo.ObjectID)]); 7 | check(fields, [[String]]); 8 | check(options, {skip: Match.Integer, limit: Match.Integer, sort: Object}); 9 | check(rowsPerPage, Match.Integer); 10 | 11 | var collection; 12 | var selector; 13 | 14 | if (_.isFunction(collectionOrFunction)) { 15 | collection = collectionOrFunction.call(this); 16 | } else { 17 | collection = collectionOrFunction; 18 | } 19 | 20 | if (!(collection instanceof Mongo.Collection)) { 21 | console.log("ReactiveTable.publish: no collection to publish"); 22 | return []; 23 | } 24 | 25 | if (_.isFunction(selectorOrFunction)) { 26 | selector = selectorOrFunction.call(this); 27 | } else { 28 | selector = selectorOrFunction; 29 | } 30 | var self = this; 31 | var filterQuery = _.extend(getFilterQuery(filters, fields, settings), selector); 32 | if (settings && settings.fields) { 33 | options.fields = settings.fields; 34 | } 35 | var pageCursor = collection.find(filterQuery, options); 36 | var fullCursor = collection.find(filterQuery); 37 | var count = fullCursor.count(); 38 | 39 | var getRow = function (row, index) { 40 | return _.extend({ 41 | "reactive-table-id": publicationId, 42 | "reactive-table-sort": index 43 | }, row); 44 | }; 45 | 46 | var getRows = function () { 47 | return _.map(pageCursor.fetch(), getRow); 48 | }; 49 | var rows = {}; 50 | _.each(getRows(), function (row) { 51 | rows[row._id] = row; 52 | }); 53 | 54 | var updateRows = function () { 55 | var newRows = getRows(); 56 | _.each(newRows, function (row, index) { 57 | var oldRow = rows[row._id]; 58 | if (oldRow) { 59 | if (!_.isEqual(oldRow, row)) { 60 | self.changed("reactive-table-rows-" + publicationId, row._id, row); 61 | rows[row._id] = row; 62 | } 63 | } else { 64 | self.added("reactive-table-rows-" + publicationId, row._id, row); 65 | rows[row._id] = row; 66 | } 67 | }); 68 | }; 69 | 70 | self.added("reactive-table-counts", publicationId, {count: count}); 71 | _.each(rows, function (row) { 72 | self.added("reactive-table-rows-" + publicationId, row._id, row); 73 | }); 74 | 75 | if (!(settings || {}).disableRowReactivity) { 76 | var initializing = true; 77 | 78 | var handle = pageCursor.observeChanges({ 79 | added: function (id, fields) { 80 | if (!initializing) { 81 | updateRows(); 82 | } 83 | }, 84 | 85 | removed: function (id, fields) { 86 | self.removed("reactive-table-rows-" + publicationId, id); 87 | delete rows[id]; 88 | updateRows(); 89 | }, 90 | 91 | changed: function (id, fields) { 92 | updateRows(); 93 | } 94 | 95 | }); 96 | } 97 | 98 | if (!(settings || {}).disablePageCountReactivity) { 99 | var countHandle = fullCursor.observeChanges({ 100 | added: function (id, fields) { 101 | if (!initializing) { 102 | self.changed("reactive-table-counts", publicationId, {count: fullCursor.count()}); 103 | } 104 | }, 105 | 106 | removed: function (id, fields) { 107 | self.changed("reactive-table-counts", publicationId, {count: fullCursor.count()}); 108 | } 109 | }); 110 | } 111 | initializing = false; 112 | 113 | self.ready(); 114 | 115 | self.onStop(function () { 116 | if (handle) handle.stop(); 117 | if (countHandle) countHandle.stop(); 118 | }); 119 | }); 120 | }; 121 | -------------------------------------------------------------------------------- /lib/sort.js: -------------------------------------------------------------------------------- 1 | normalizeSort = function (field, oldField) { 2 | // preserve user sort settings 3 | if (oldField && _.has(oldField, 'sortOrder')) { 4 | field.sortOrder = oldField.sortOrder; 5 | } 6 | if (oldField && _.has(oldField, 'sortDirection')) { 7 | field.sortDirection = oldField.sortDirection; 8 | } 9 | 10 | // backwards-compatibility 11 | if (!_.has(field, 'sortOrder') && _.has(field, 'sort')) { 12 | console.warn('reactiveTable warning: The "sort" option for fields is deprecated'); 13 | field.sortOrder = 0; 14 | field.sortDirection = field.sort; 15 | } 16 | 17 | 18 | var sortOrder; 19 | 20 | if (!_.has(field, 'sortOrder')) { 21 | sortOrder = Infinity; 22 | field.sortOrder = new ReactiveVar(); 23 | } else if (field.sortOrder instanceof ReactiveVar) { 24 | sortOrder = field.sortOrder.get() 25 | } else { 26 | sortOrder = field.sortOrder; 27 | field.sortOrder = new ReactiveVar(); 28 | } 29 | 30 | if (!_.isNumber(sortOrder) || sortOrder < 0) { 31 | console.error('reactiveTable error - sortOrder must be a postive number: ' + sortOrder); 32 | sortOrder = Infinity; 33 | } 34 | field.sortOrder.set(sortOrder); 35 | 36 | var sortDirection; 37 | 38 | if (!_.has(field, 'sortDirection')) { 39 | sortDirection = 1; 40 | field.sortDirection = new ReactiveVar() 41 | } else if (field.sortDirection instanceof ReactiveVar) { 42 | sortDirection = field.sortDirection.get(); 43 | } else { 44 | sortDirection = field.sortDirection; 45 | field.sortDirection = new ReactiveVar(); 46 | } 47 | 48 | if (sortDirection === 'desc' || sortDirection === 'descending' || sortDirection === -1) { 49 | sortDirection = -1; 50 | } else if (sortDirection) { 51 | sortDirection = 1; 52 | } 53 | field.sortDirection.set(sortDirection); 54 | }; 55 | 56 | getSortedFields = function (fields, multiColumnSort) { 57 | var filteredFields = _.filter(fields, function (field) { 58 | return field.sortOrder.get() < Infinity; 59 | }); 60 | if (!filteredFields.length) { 61 | var firstSortableField = _.find(fields, function (field) { 62 | return _.isUndefined(field.sortable) || field.sortable !== false; 63 | }); 64 | if (firstSortableField) { 65 | filteredFields = [firstSortableField]; 66 | } 67 | } 68 | var sortedFields = _.sortBy(filteredFields, function (field) { 69 | return field.sortOrder.get(); 70 | }); 71 | return multiColumnSort ? sortedFields : sortedFields.slice(0, 1); 72 | } 73 | 74 | getSortQuery = function (fields, multiColumnSort) { 75 | var sortedFields = getSortedFields(fields, multiColumnSort); 76 | var sortQuery = {}; 77 | _.each(sortedFields, function (field) { 78 | sortQuery[field.key] = field.sortDirection.get(); 79 | }); 80 | return sortQuery; 81 | }; 82 | 83 | sortWithFunctions = function (rows, fields, multiColumnSort) { 84 | var sortedFields = getSortedFields(fields, multiColumnSort); 85 | var sortedRows = rows; 86 | 87 | _.each(sortedFields.reverse(), function (field) { 88 | if (field.sortFn) { 89 | sortedRows = _.sortBy(sortedRows, function (row) { 90 | return field.sortFn( get( row, field.key ), row ); 91 | }); 92 | } else if (field.sortByValue || !field.fn) { 93 | sortedRows = _.sortBy(sortedRows, field.key); 94 | } else { 95 | sortedRows = _.sortBy(sortedRows, function (row) { 96 | return field.fn( get( row, field.key ), row ); 97 | }); 98 | } 99 | if (field.sortDirection.get() === -1) { 100 | sortedRows.reverse(); 101 | } 102 | }); 103 | return sortedRows; 104 | }; 105 | 106 | getPrimarySortField = function (fields, multiColumnSort) { 107 | return getSortedFields(fields, multiColumnSort)[0]; 108 | }; 109 | 110 | changePrimarySort = function(fieldId, fields, multiColumnSort) { 111 | var primarySortField = getPrimarySortField(fields, multiColumnSort); 112 | if (primarySortField && primarySortField.fieldId === fieldId) { 113 | var sortDirection = -1 * primarySortField.sortDirection.get(); 114 | primarySortField.sortDirection.set(sortDirection); 115 | primarySortField.sortOrder.set(0); 116 | } else { 117 | _.each(fields, function (field) { 118 | if (field.fieldId === fieldId) { 119 | field.sortOrder.set(0); 120 | if (primarySortField) { 121 | field.sortDirection.set(primarySortField.sortDirection.get()); 122 | } 123 | } else { 124 | var sortOrder = 1 + field.sortOrder.get(); 125 | field.sortOrder.set(sortOrder); 126 | } 127 | }); 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /lib/reactive_table.html: -------------------------------------------------------------------------------- 1 | 127 | -------------------------------------------------------------------------------- /test/test_multiple_tables.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('Multiple tables - rendering', function (test) { 2 | testTable( 3 | {collection: rows}, 4 | function () { 5 | testTable( 6 | {collection: collection}, 7 | function () { 8 | test.length($('.reactive-table'), 2, "two tables should be rendered"); 9 | test.length($('.reactive-table:nth-of-type(1) tbody tr'), 6, "the first table should have 6 rows"); 10 | test.length($('.reactive-table:nth-of-type(2) tbody tr'), 6, "the second table should have 6 rows"); 11 | } 12 | ); 13 | } 14 | ); 15 | }); 16 | 17 | Tinytest.add('Multiple tables - settings', function (test) { 18 | testTable( 19 | {collection: rows, settings: {showNavigation: true}}, 20 | function () { 21 | testTable( 22 | {collection: collection, settings: {showNavigation: false}}, 23 | function () { 24 | test.length($('.reactive-table'), 2, "two tables should be rendered"); 25 | test.length($('.reactive-table-navigation'), 1, "only one table should have navigation"); 26 | } 27 | ); 28 | } 29 | ); 30 | 31 | testTable( 32 | {collection: rows, settings: {rowsPerPage: 5}}, 33 | function () { 34 | testTable( 35 | {collection: collection, settings: {rowsPerPage: 2}}, 36 | function () { 37 | test.length($('.reactive-table'), 2, "two tables should be rendered"); 38 | test.length($('.reactive-table:nth-of-type(1) tbody tr'), 5, "the first table should have 5 rows"); 39 | test.length($('.reactive-table:nth-of-type(2) tbody tr'), 2, "the second table should have 2 rows"); 40 | } 41 | ); 42 | } 43 | ); 44 | }); 45 | 46 | testAsyncMulti('Multiple tables - sorting', [function (test, expect) { 47 | var table1 = Blaze.renderWithData( 48 | Template.reactiveTable, 49 | {collection: rows}, 50 | document.body 51 | ); 52 | var table2 = Blaze.renderWithData( 53 | Template.reactiveTable, 54 | {collection: rows}, 55 | document.body 56 | ); 57 | test.equal($('.reactive-table:nth-of-type(1) tbody tr:first-child td:first-child').text(), "Ada Lovelace", "first table sorted ascending"); 58 | test.equal($('.reactive-table:nth-of-type(2) tbody tr:first-child td:first-child').text(), "Ada Lovelace", "second table sorted ascending"); 59 | 60 | var expectSecondTableDescending = expect(function () { 61 | test.equal($('.reactive-table:nth-of-type(1) tbody tr:first-child td:first-child').text(), "Ada Lovelace", "first table sorted ascending"); 62 | test.equal($('.reactive-table:nth-of-type(2) tbody tr:first-child td:first-child').text(), "Nikola Tesla", "second table sorted descending"); 63 | Blaze.remove(table1); 64 | Blaze.remove(table2); 65 | }); 66 | 67 | $('.reactive-table:nth-of-type(2) th:first-child').click(); 68 | Meteor.setTimeout(expectSecondTableDescending, 0); 69 | }]); 70 | 71 | testAsyncMulti('Multiple tables - filtering', [function (test, expect) { 72 | var table1 = Blaze.renderWithData( 73 | Template.reactiveTable, 74 | {collection: rows}, 75 | document.body 76 | ); 77 | var table2 = Blaze.renderWithData( 78 | Template.reactiveTable, 79 | {collection: rows}, 80 | document.body 81 | ); 82 | test.length($('.reactive-table:nth-of-type(1) tbody tr'), 6, "first table should have 6 rows"); 83 | test.length($('.reactive-table:nth-of-type(2) tbody tr'), 6, "second table should have 6 rows"); 84 | 85 | var expectFirstTableFiltered = expect(function () { 86 | test.length($('.reactive-table:nth-of-type(1) tbody tr'), 1, "first table should have 1 row"); 87 | test.length($('.reactive-table:nth-of-type(2) tbody tr'), 6, "second table should have 6 rows"); 88 | Blaze.remove(table1); 89 | Blaze.remove(table2); 90 | }); 91 | 92 | $($('.reactive-table-filter input')[0]).val("carl"); 93 | $($('.reactive-table-filter input')[0]).trigger('input'); 94 | Meteor.setTimeout(expectFirstTableFiltered, 1000); 95 | }]); 96 | 97 | testAsyncMulti('Multiple tables - pagination', [function (test, expect) { 98 | var table1 = Blaze.renderWithData( 99 | Template.reactiveTable, 100 | {collection: rows, settings: {rowsPerPage: 2}}, 101 | document.body 102 | ); 103 | var table2 = Blaze.renderWithData( 104 | Template.reactiveTable, 105 | {collection: rows, settings: {rowsPerPage: 2}}, 106 | document.body 107 | ); 108 | test.equal($($('.reactive-table-navigation .page-number input')[0]).val(), "1", "first table on page 1"); 109 | test.equal($($('.reactive-table-navigation .page-number input')[1]).val(), "1", "second table on page 1"); 110 | 111 | var expectSecondTablePageTwo = expect(function () { 112 | test.equal($($('.reactive-table-navigation .page-number input')[0]).val(), "1", "first table on page 1"); 113 | test.equal($($('.reactive-table-navigation .page-number input')[1]).val(), "2", "second table on page 2"); 114 | Blaze.remove(table1); 115 | Blaze.remove(table2); 116 | }); 117 | 118 | $($('.reactive-table-navigation .next-page')[1]).click(); 119 | Meteor.setTimeout(expectSecondTablePageTwo, 0); 120 | }]); 121 | 122 | testAsyncMulti('Multiple tables - server-side collection', [function (test, expect) { 123 | var table1 = Blaze.renderWithData( 124 | Template.reactiveTable, 125 | {collection: 'collection', fields: ['name', 'score']}, 126 | document.body 127 | ); 128 | 129 | var table2 = Blaze.renderWithData( 130 | Template.reactiveTable, 131 | {collection: 'collection', fields: ['name', 'score']}, 132 | document.body 133 | ); 134 | 135 | var expectSixRowsEach = expect(function () { 136 | test.length($('.reactive-table tbody tr'), 12, " 2 rendered tables should have 6 rows each"); 137 | Blaze.remove(table1); 138 | Blaze.remove(table2); 139 | }); 140 | 141 | Meteor.setTimeout(expectSixRowsEach, 500); 142 | }]); -------------------------------------------------------------------------------- /lib/reactive_table_i18n.js: -------------------------------------------------------------------------------- 1 | i18n.map('en', { 2 | reactiveTable: { 3 | filter: 'Filter', 4 | columns: 'Columns', 5 | show: 'Show', 6 | rowsPerPage: 'rows per page', 7 | page: 'Page', 8 | of: 'of' 9 | } 10 | }); 11 | 12 | i18n.map('fr', { 13 | reactiveTable: { 14 | filter: 'Filtre', 15 | columns: 'Colonnes', 16 | show: 'Voir', 17 | rowsPerPage: 'lignes par page', 18 | page: 'page', 19 | of: 'sur' 20 | } 21 | }); 22 | 23 | i18n.map('ru', { 24 | reactiveTable: { 25 | filter: 'Фильтр', 26 | columns: 'Колонки', 27 | show: 'Показать', 28 | rowsPerPage: 'строк на странице', 29 | page: 'Страница', 30 | of: 'из' 31 | } 32 | }); 33 | 34 | i18n.map('es', { 35 | reactiveTable: { 36 | filter: 'Filtro', 37 | columns: 'Columnas', 38 | show: 'Mostrar', 39 | rowsPerPage: 'filas por página', 40 | page: 'Página', 41 | of: 'de' 42 | } 43 | }); 44 | 45 | i18n.map('nl', { 46 | reactiveTable: { 47 | filter: 'Filter', 48 | show: 'Toon', 49 | rowsPerPage: 'regels per pagina', 50 | page: 'Pagina', 51 | of: 'van' 52 | } 53 | }); 54 | 55 | i18n.map('pt-br', { 56 | reactiveTable: { 57 | filter: 'Filtro', 58 | show: 'Mostrar', 59 | rowsPerPage: 'linhas por página', 60 | page: 'Página', 61 | of: 'de' 62 | } 63 | }); 64 | 65 | i18n.map('pt', { 66 | reactiveTable: { 67 | filter: 'Filtro', 68 | show: 'Mostrar', 69 | rowsPerPage: 'linhas por página', 70 | page: 'Página', 71 | of: 'de' 72 | } 73 | }); 74 | 75 | i18n.map('it', { 76 | reactiveTable: { 77 | filter: 'Filtra', 78 | show: 'Mostra', 79 | rowsPerPage: 'righe per pagina', 80 | page: 'Pagina', 81 | of: 'di' 82 | } 83 | }); 84 | 85 | i18n.map('sv', { 86 | reactiveTable: { 87 | filter: 'Filter', 88 | show: 'Visa', 89 | rowsPerPage: 'rader per sida', 90 | page: 'Sida', 91 | of: 'av' 92 | } 93 | }); 94 | 95 | i18n.map('ua', { 96 | reactiveTable: { 97 | filter: 'Фільтр', 98 | show: 'Показати', 99 | rowsPerPage: 'рядків на сторінці', 100 | page: 'Сторінка', 101 | of: 'з' 102 | } 103 | }); 104 | 105 | i18n.map('tr', { 106 | reactiveTable: { 107 | filter: 'Süz', 108 | columns: 'Sütunlar', 109 | show: 'Sayfa başına', 110 | rowsPerPage: 'satır göster', 111 | page: 'Sayfa', 112 | of: ' / ' 113 | } 114 | }); 115 | 116 | i18n.map('sk', { 117 | reactiveTable: { 118 | filter: 'Filter', 119 | show: 'Zobraz', 120 | rowsPerPage: 'riadkov na stranu', 121 | page: 'Strana', 122 | of: 'z' 123 | } 124 | }); 125 | 126 | i18n.map('cs', { 127 | reactiveTable: { 128 | filter: 'Filter', 129 | show: 'Zobraz', 130 | rowsPerPage: 'řádků na stranu', 131 | page: 'Strana', 132 | of: 'z' 133 | } 134 | }); 135 | 136 | i18n.map('he', { 137 | reactiveTable: { 138 | filter: 'פילטר', 139 | show: 'הצג', 140 | rowsPerPage: 'שורות לעמוד', 141 | page: 'עמוד', 142 | of: 'מתוך' 143 | } 144 | }); 145 | 146 | i18n.map('da', { 147 | reactiveTable: { 148 | filter: 'Filter', 149 | columns: 'Kolonner', 150 | show: 'Vis', 151 | rowsPerPage: 'rækker per side', 152 | page: 'Side', 153 | of: 'af' 154 | } 155 | }); 156 | 157 | i18n.map('de', { 158 | reactiveTable: { 159 | filter: 'Filter', 160 | columns: 'Spalten', 161 | show: 'Zeige', 162 | rowsPerPage: 'Zeilen pro Seite', 163 | page: 'Seite', 164 | of: 'von' 165 | } 166 | }); 167 | 168 | i18n.map('fi', { 169 | reactiveTable: { 170 | filter: 'Suodata', 171 | show: 'Näytä', 172 | rowsPerPage: 'riviä sivulla', 173 | page: 'Sivu', 174 | of: ' / ' 175 | } 176 | }); 177 | 178 | i18n.map('no', { 179 | reactiveTable: { 180 | filter: 'Filter', 181 | columns: 'Kolonner', 182 | show: 'Vis', 183 | rowsPerPage: 'rader per side', 184 | page: 'Side', 185 | of: 'av' 186 | } 187 | }); 188 | 189 | i18n.map('pl', { 190 | reactiveTable: { 191 | filter: 'Szukaj', 192 | columns: 'Kolumny', 193 | show: 'Pokaż', 194 | rowsPerPage: 'pozycji na stronie', 195 | page: 'Strona', 196 | of: 'z' 197 | } 198 | }); 199 | 200 | i18n.map('hr', { 201 | reactiveTable: { 202 | filter: 'Filter', 203 | columns: 'Stupci', 204 | show: 'Prikaži', 205 | rowsPerPage: 'redova po stranici', 206 | page: 'Stranica', 207 | of: 'od' 208 | } 209 | }); 210 | 211 | i18n.map('is', { 212 | reactiveTable: { 213 | filter: 'Sía', 214 | columns: 'Dálkar', 215 | show: 'Sýna', 216 | rowsPerPage: 'raðir á síðu', 217 | page: 'Síða', 218 | of: 'af' 219 | } 220 | }); 221 | 222 | i18n.map('zh', { 223 | reactiveTable: { 224 | filter: '过滤', 225 | columns: '列', 226 | show: '显示', 227 | rowsPerPage: '每页行数', 228 | page: '页数', 229 | of: '之' 230 | } 231 | }); 232 | 233 | i18n.map('zh-tw', { 234 | reactiveTable: { 235 | filter: '過濾', 236 | columns: '列', 237 | show: '顯示', 238 | rowsPerPage: '每頁行數', 239 | page: '頁數', 240 | of: '之' 241 | } 242 | }); 243 | 244 | i18n.map('fa', { 245 | reactiveTable: { 246 | filter: 'تزکیه', 247 | columns: 'ستون', 248 | show: 'ارائه', 249 | rowsPerPage: 'ردیف در هر صفحه', 250 | page: 'صفحه', 251 | of: 'از' 252 | } 253 | }); 254 | 255 | i18n.map('gr', { 256 | reactiveTable: { 257 | filter: 'Φίλτρα', 258 | columns: 'Στήλες', 259 | show: 'Προβολή', 260 | rowsPerPage: 'γραμμές ανά σελίδα', 261 | page: 'Σελίδα', 262 | of: 'από' 263 | } 264 | }); 265 | 266 | i18n.map('bg', { 267 | reactiveTable: { 268 | filter: 'Филтър', 269 | columns: 'Колони', 270 | show: 'Покажи', 271 | rowsPerPage: 'реда на страница', 272 | page: 'Страница', 273 | of: 'от' 274 | } 275 | }); 276 | 277 | i18n.map('mk', { 278 | reactiveTable: { 279 | filter: 'Филтер', 280 | columns: 'Колони', 281 | show: 'Покажи', 282 | rowsPerPage: 'Редови на страница', 283 | page: 'Страница', 284 | of: 'од' 285 | } 286 | }); 287 | 288 | i18n.map('ro', { 289 | reactiveTable: { 290 | filter: 'Filtru', 291 | columns: 'Coloane', 292 | show: 'Arată', 293 | rowsPerPage: 'rânduri per pagină', 294 | page: 'Pagină', 295 | of: 'din' 296 | } 297 | }); 298 | 299 | i18n.map('ar', { 300 | reactiveTable: { 301 | filter: 'رشح', 302 | columns: 'الأعمدة', 303 | show: 'اظهر', 304 | rowsPerPage: 'الصفوف بالصفحة', 305 | page: 'الصفحة', 306 | of: 'من' 307 | } 308 | }); -------------------------------------------------------------------------------- /lib/filter.js: -------------------------------------------------------------------------------- 1 | var parseFilterString = function (filterString) { 2 | var startQuoteRegExp = /^[\'\"]/; 3 | var endQuoteRegExp = /[\'\"]$/; 4 | var filters = []; 5 | var words = filterString.split(' '); 6 | 7 | var inQuote = false; 8 | var quotedWord = ''; 9 | _.each(words, function (word) { 10 | if (inQuote) { 11 | if (endQuoteRegExp.test(word)) { 12 | filters.push(quotedWord + ' ' + word.slice(0, word.length - 1)); 13 | inQuote = false; 14 | quotedWord = ''; 15 | } else { 16 | quotedWord = quotedWord + ' ' + word; 17 | } 18 | } else if (startQuoteRegExp.test(word)) { 19 | if (endQuoteRegExp.test(word)) { 20 | filters.push(word.slice(1, word.length - 1)); 21 | } else { 22 | inQuote = true; 23 | quotedWord = word.slice(1, word.length); 24 | } 25 | } else { 26 | filters.push(word); 27 | } 28 | }); 29 | return filters; 30 | }; 31 | 32 | var escapeRegex = function(text) { 33 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 34 | }; 35 | 36 | var getFieldMatches = function (field) { 37 | var fieldMatches = []; 38 | var keys = field.split('.'); 39 | var previousKeys = ''; 40 | _.each(keys, function (key) { 41 | fieldMatches.push(previousKeys + key); 42 | previousKeys += key + '.'; 43 | }); 44 | var extraMatch = field.replace(/\.\d+\./g, "."); 45 | if (fieldMatches.indexOf(extraMatch) === -1) fieldMatches.push(extraMatch); 46 | return fieldMatches; 47 | }; 48 | 49 | getFilterQuery = function (filterInputs, filterFields, settings) { 50 | settings = settings || {}; 51 | if (settings.enableRegex === undefined) { 52 | settings.enableRegex = false; 53 | } 54 | if (settings.filterOperator === undefined) { 55 | settings.filterOperator = "$and"; 56 | } 57 | if (settings.fields) { 58 | _.each(filterInputs, function (filter, index) { 59 | if (_.any(settings.fields, function (include) { return include; })) { 60 | filterFields[index] = _.filter(filterFields[index], function (field) { 61 | return _.any(getFieldMatches(field), function (fieldMatch) { 62 | // ensure that the _id field is filtered on, even if it is not explicitly mentioned 63 | if (fieldMatch === "_id") return true; 64 | return settings.fields[fieldMatch]; 65 | }); 66 | }); 67 | } else { 68 | filterFields[index] = _.filter(filterFields[index], function (field) { 69 | return _.all(getFieldMatches(field), function (fieldMatch) { 70 | return _.isUndefined(settings.fields[fieldMatch]) || settings.fields[fieldMatch]; 71 | }); 72 | }); 73 | } 74 | }); 75 | } 76 | var numberRegExp = /^\d+$/; 77 | var queryList = []; 78 | _.each(filterInputs, function (filter, index) { 79 | if (filter) { 80 | if (_.isObject(filter)) { 81 | var fieldQueries = _.map(filterFields[index], function (field) { 82 | var query = {}; 83 | query[field] = filter; 84 | return query; 85 | }); 86 | if (fieldQueries.length) { 87 | queryList.push({'$or': fieldQueries}); 88 | } 89 | } else { 90 | var filters = parseFilterString(filter); 91 | _.each(filters, function (filterWord) { 92 | if (settings.enableRegex === false) { 93 | filterWord = escapeRegex(filterWord); 94 | } 95 | var filterQueryList = []; 96 | _.each(filterFields[index], function (field) { 97 | var filterRegExp = new RegExp(filterWord, 'i'); 98 | var query = {}; 99 | query[field] = filterRegExp; 100 | filterQueryList.push(query); 101 | 102 | if (numberRegExp.test(filterWord)) { 103 | var numberQuery = {}; 104 | numberQuery[field] = parseInt(filterWord, 10); 105 | filterQueryList.push(numberQuery); 106 | } 107 | 108 | if (filterWord === "true") { 109 | var boolQuery = {}; 110 | boolQuery[field] = true; 111 | filterQueryList.push(boolQuery); 112 | } else if (filterWord === "false") { 113 | var boolQuery = {}; 114 | boolQuery[field] = false; 115 | filterQueryList.push(boolQuery); 116 | } 117 | }); 118 | 119 | if (filterQueryList.length) { 120 | var filterQuery = {'$or': filterQueryList}; 121 | queryList.push(filterQuery); 122 | } 123 | }); 124 | } 125 | } 126 | }); 127 | 128 | var query = {}; 129 | 130 | if(queryList.length) { 131 | query[settings.filterOperator] = queryList; 132 | } 133 | 134 | return query; 135 | }; 136 | 137 | if (Meteor.isClient) { 138 | ReactiveTable = ReactiveTable || {}; 139 | 140 | var reactiveTableFilters = {}; 141 | var callbacks = {}; 142 | 143 | ReactiveTable.Filter = function (id, fields) { 144 | if (reactiveTableFilters[id]) { 145 | reactiveTableFilters[id].fields = fields; 146 | return reactiveTableFilters[id]; 147 | } 148 | 149 | var filter = new ReactiveVar(); 150 | 151 | this.fields = fields; 152 | 153 | this.get = function () { 154 | return filter.get() || ''; 155 | }; 156 | 157 | this.set = function (filterString) { 158 | filter.set(filterString); 159 | _.each(callbacks[id], function (callback) { 160 | callback(); 161 | }); 162 | }; 163 | 164 | reactiveTableFilters[id] = this; 165 | }; 166 | 167 | ReactiveTable.clearFilters = function (filterIds) { 168 | _.each(filterIds, function (filterId) { 169 | if (reactiveTableFilters[filterId]) { 170 | reactiveTableFilters[filterId].set(''); 171 | } 172 | }); 173 | }; 174 | 175 | dependOnFilters = function (filterIds, callback) { 176 | _.each(filterIds, function (filterId) { 177 | if (_.isUndefined(callbacks[filterId])) { 178 | callbacks[filterId] = []; 179 | } 180 | callbacks[filterId].push(callback); 181 | }); 182 | }; 183 | 184 | getFilterStrings = function (filterIds) { 185 | return _.map(filterIds, function (filterId) { 186 | if (_.isUndefined(reactiveTableFilters[filterId])) { 187 | reactiveTableFilters[filterId] = new ReactiveTable.Filter(filterId); 188 | } 189 | return reactiveTableFilters[filterId].get(); 190 | }); 191 | }; 192 | 193 | getFilterFields = function (filterIds, allFields) { 194 | return _.map(filterIds, function (filterId) { 195 | if (_.isUndefined(reactiveTableFilters[filterId])) { 196 | return _.map(allFields, function (field) { return field.key; }); 197 | } else if (_.isEmpty(reactiveTableFilters[filterId].fields)) { 198 | return _.map(allFields, function (field) { return field.key; }); 199 | } else { 200 | return reactiveTableFilters[filterId].fields; 201 | } 202 | }); 203 | }; 204 | 205 | Template.reactiveTableFilter.helpers({ 206 | 'class': function () { 207 | return this.class || 'input-group'; 208 | }, 209 | 210 | 'filter': function () { 211 | if (_.isUndefined(reactiveTableFilters[this.id])) { 212 | new ReactiveTable.Filter(this.id, this.fields); 213 | } else if (_.isUndefined(reactiveTableFilters[this.id].fields)) { 214 | reactiveTableFilters[this.id].fields = this.fields; 215 | } 216 | return reactiveTableFilters[this.id].get(); 217 | } 218 | }); 219 | 220 | var updateFilter = _.debounce(function (template, filterText) { 221 | reactiveTableFilters[template.data.id].set(filterText); 222 | }, 200); 223 | 224 | Template.reactiveTableFilter.events({ 225 | 'keyup .reactive-table-input, input .reactive-table-input': function (event) { 226 | var template = Template.instance(); 227 | var filterText = $(event.target).val(); 228 | updateFilter(template, filterText); 229 | }, 230 | }); 231 | } 232 | -------------------------------------------------------------------------------- /examples/table-features/table-features.js: -------------------------------------------------------------------------------- 1 | var Tables = new Meteor.Collection('features'); 2 | 3 | if (Meteor.isClient) { 4 | 5 | var checkOrX = function (value) { 6 | var html; 7 | // first, normalize the value to a canonical interpretation 8 | if (typeof value === 'boolean') 9 | value = { 10 | support: value 11 | }; 12 | 13 | if (value === null || value === undefined) { 14 | html = '?'; 15 | } else { 16 | if (value.support === true) 17 | html = '' 18 | else if (value.support === false) 19 | html = ''; 20 | else 21 | html = '' + value.support + ''; 22 | if (value.link) 23 | html += ' (more)'; 24 | } 25 | return new Spacebars.SafeString(html); 26 | }; 27 | 28 | Template.featureComparison.helpers({ 29 | tables : function () { 30 | return Tables; 31 | }, 32 | 33 | tableSettings : function () { 34 | return { 35 | rowsPerPage: 5, 36 | showNavigation: 'auto', 37 | showColumnToggles: true, 38 | fields: [ 39 | { 40 | key: 'name', 41 | label: 'Library', 42 | fn: function (name, object) { 43 | var html = '' + name + ''; 44 | return new Spacebars.SafeString(html); 45 | } 46 | }, 47 | { key: 'multisort', label: 'Multi-column sorting', fn: checkOrX }, 48 | { key: 'pages', label: 'Pagination', fn: checkOrX }, 49 | { key: 'filter', label: 'Filtering/Search', fn: checkOrX }, 50 | { key: 'resize', label: 'Resizable Columns', fn: checkOrX }, 51 | { key: 'edit', label: 'Inline Editing', fn: checkOrX }, 52 | { key: 'responsive', label: 'Mobile/Responsive', fn: checkOrX }, 53 | { key: 'i18n', label: 'Internationalization', fn: checkOrX, hidden: true }, 54 | { key: 'keyboard', label: 'Keyboard navigation', fn: checkOrX, hidden: true }, 55 | { key: 'plugins', label: 'Plugins', fn: checkOrX, hidden: true }, 56 | { key: 'meteor', label: 'Meteor Integration', fn: checkOrX, hidden: true }, 57 | { key: 'lastUpdate', label: 'Last update', fn: checkOrX } 58 | ] 59 | }; 60 | } 61 | }); 62 | } 63 | 64 | if (Meteor.isServer) { 65 | Meteor.startup(function () { 66 | Tables.remove({'name': {'$exists': true}}); 67 | 68 | Tables.insert({ 69 | name: 'reactive-table', 70 | url: 'https://github.com/ecohealthalliance/reactive-table', 71 | multisort: false, 72 | pages: true, 73 | filter: true, 74 | resize: false, 75 | edit: false, 76 | responsive: false, 77 | i18n: true, 78 | keyboard: false, 79 | plugins: undefined, 80 | meteor: true 81 | }); 82 | 83 | Tables.insert({ 84 | name: 'DataTables', 85 | url: 'https://datatables.net/', 86 | multisort: {support: true, link: 'http://www.datatables.net/examples/basic_init/multi_col_sort.html'}, 87 | pages: true, 88 | filter: true, 89 | resize: true, 90 | edit: true, 91 | responsive: {support: true, link: 'https://datatables.net/release-datatables/examples/basic_init/flexible_width.html'}, 92 | i18n: true, 93 | keyboard: {support: true, link: 'http://datatables.net/release-datatables/extras/KeyTable/'}, 94 | plugins: {support: 'plugins & extensions', link: 'https://datatables.net/plug-ins/'}, 95 | meteor: {support: 'partial', 'link': 'https://github.com/ecohealthalliance/reactive-table/issues/10#issuecomment-35941155'}, 96 | lastUpdate: {support: 'probably today', link: 'https://github.com/DataTables/DataTables'} 97 | }); 98 | 99 | Tables.insert({ 100 | name: 'SlickGrid', 101 | url: 'https://github.com/mleibman/SlickGrid', 102 | multisort: {support: true, link: 'http://mleibman.github.io/SlickGrid/examples/example-multi-column-sort.html'}, 103 | filter: true, 104 | resize: true, 105 | edit: true, 106 | responsive: undefined, 107 | i18n: undefined, 108 | keyboard: undefined, 109 | plugins: {support: true, link: 'https://github.com/mleibman/SlickGrid/wiki/Examples'}, 110 | meteor: {support: 'partial', link: 'https://github.com/ecohealthalliance/reactive-table/issues/10#issuecomment-35941155'}, 111 | lastUpdate: {support: 'stalled', link: 'https://github.com/mleibman/SlickGrid'} 112 | }); 113 | 114 | Tables.insert({ 115 | name: 'Dynatable', 116 | url: 'http://www.dynatable.com/', 117 | multisort: {support: true, link: 'http://www.dynatable.com/#sorting'}, 118 | pages: true, 119 | filter: true, 120 | resize: false, 121 | edit: false, 122 | responsive: true, 123 | i18n: undefined, 124 | keyboard: false, 125 | plugins: undefined, 126 | meteor: {support: false, 'link': 'https://github.com/alfajango/jquery-dynatable/issues/59'} 127 | }); 128 | 129 | Tables.insert({ 130 | name: 'tablesorter', 131 | url: 'https://github.com/Mottie/tablesorter', 132 | multisort: {support: true, link: 'https://github.com/Mottie/tablesorter#features'}, 133 | pages: {support: true, link: 'http://mottie.github.io/tablesorter/docs/example-pager.html'}, 134 | filter: true, 135 | edit: true, 136 | responsive: undefined, 137 | i18n: undefined, 138 | keyboard: undefined, 139 | plugins: undefined, 140 | meteor: undefined 141 | }); 142 | 143 | Tables.insert({ 144 | name: 'Handsontable', 145 | url: 'http://handsontable.com/', 146 | multisort: {support: false, link: 'https://github.com/handsontable/jquery-handsontable/wiki/Understanding-column-sorting-plugin'}, 147 | pages: {support: true, link: 'http://handsontable.com/demo/pagination.html'}, 148 | filter: {support: true, link: 'http://handsontable.com/demo/search.html'}, 149 | resize: {support: true, link: 'http://handsontable.com/demo/column_resize.html'}, 150 | edit: true, 151 | responsive: undefined, 152 | i18n: undefined, 153 | keyboard: true, 154 | plugins: undefined, 155 | meteor: {support: true, link: 'https://github.com/olragon/meteor-handsontable/'} 156 | }); 157 | 158 | Tables.insert({ 159 | name: 'jqWidgets jqxGrid', 160 | url: 'http://www.jqwidgets.com/jquery-widgets-demo/demos/jqxgrid/index.htm', 161 | multisort: {support: false, link: 'http://www.jqwidgets.com/community/topic/sorting-by-multiple-columns/'}, 162 | pages: {support: true, link: 'http://www.jqwidgets.com/jquery-widgets-demo/demos/jqxgrid/index.htm#demos/jqxgrid/paging.htm'}, 163 | filter: {support: true, link: 'http://www.jqwidgets.com/jquery-widgets-demo/demos/jqxgrid/index.htm#demos/jqxgrid/filtering.htm'}, 164 | resize: {support: true, link: 'http://www.jqwidgets.com/jquery-widgets-demo/demos/jqxgrid/index.htm#demos/jqxgrid/columnsresizing.htm'}, 165 | edit: {support: true, link: 'http://www.jqwidgets.com/jquery-widgets-demo/demos/jqxgrid/index.htm#demos/jqxgrid/spreadsheet.htm'}, 166 | responsive: false, 167 | i18n: {support: true, link: 'http://www.jqwidgets.com/jquery-widgets-demo/demos/jqxgrid/index.htm#demos/jqxgrid/localization.htm'}, 168 | keyboard: {support: true, link: 'http://www.jqwidgets.com/jquery-widgets-demo/demos/jqxgrid/index.htm#demos/jqxgrid/keyboardsupport.htm'}, 169 | plugins: undefined, 170 | meteor: undefined 171 | }); 172 | 173 | Tables.insert({ 174 | name: 'Backgrid.js - reactive, Backbone UI', 175 | url: 'http://backgridjs.com/', 176 | multisort: {support: false, link: 'https://github.com/wyuenho/backgrid/issues/453'}, 177 | pages: {support: true, link: 'https://github.com/wyuenho/backgrid-paginator'}, 178 | filter: {support: true, link: 'https://github.com/wyuenho/backgrid-filter'}, 179 | resize: false, 180 | edit: {support: true, link: 'http://backgridjs.com/index.html#complete-example'}, 181 | responsive: undefined, 182 | i18n: undefined, 183 | keyboard: false, 184 | plugins: undefined, 185 | meteor: {support: false, link: 'https://atmospherejs.com/?q=backgrid'} 186 | }); 187 | 188 | Tables.insert({ 189 | name: 'Tabular', 190 | url: 'https://github.com/aldeed/meteor-tabular', 191 | multisort: {support: false, link: 'https://github.com/aldeed/meteor-tabular/issues/144'}, 192 | pages: true, 193 | filter: true, 194 | resize: {support: false, link: 'https://github.com/aldeed/meteor-tabular/issues/146'}, 195 | edit: {support: false, link: 'https://github.com/aldeed/meteor-tabular/issues/145'}, 196 | responsive: true, 197 | i18n: {support: undefined, link: 'https://github.com/aldeed/meteor-tabular/issues/147'}, 198 | keyboard: {support: undefined, link: 'https://github.com/aldeed/meteor-tabular/issues/148'}, 199 | plugins: {support: true, link: 'http://datatables.net/extensions/index'}, 200 | meteor: true 201 | }); 202 | 203 | }); 204 | } 205 | -------------------------------------------------------------------------------- /test/test_pagination.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('Pagination - initial page', function (test) { 2 | testTable( 3 | {collection: rows, settings: {rowsPerPage: 2}}, 4 | function () { 5 | test.length($('.reactive-table tbody tr'), 2, "two rows should be rendered"); 6 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "should be on the first page"); 7 | test.equal($('.reactive-table-navigation .page-number input').val(), "1", "displayed page number should be 1"); 8 | test.length($('.reactive-table-navigation .page-number label .page-number-count').text().match(/3/), 1, "displayed page count should be 3"); 9 | } 10 | ); 11 | }); 12 | 13 | testAsyncMulti('Pagination - previous/next controls', [function (test, expect) { 14 | var table = Blaze.renderWithData( 15 | Template.reactiveTable, 16 | {collection: rows, settings: {rowsPerPage: 2}}, 17 | document.body 18 | ); 19 | 20 | test.length($('.reactive-table-navigation .previous-page'), 0, "first page shouldn't have previous button"); 21 | 22 | var expectBackToFirstPage = expect(function () { 23 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "should be on first page"); 24 | test.length($('.reactive-table tbody tr'), 2, "second page should have two rows"); 25 | test.equal($('.reactive-table-navigation .page-number input').val(), "1", "displayed page number should be 1"); 26 | 27 | Blaze.remove(table); 28 | }); 29 | 30 | var expectBackToSecondPage = expect(function () { 31 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Claude Shannon", "should be on the second page"); 32 | test.length($('.reactive-table tbody tr'), 2, "second page should have two rows"); 33 | test.equal($('.reactive-table-navigation .page-number input').val(), "2", "displayed page number should be 2"); 34 | 35 | $('.reactive-table-navigation .previous-page').click(); 36 | Meteor.setTimeout(expectBackToFirstPage, 0); 37 | }); 38 | 39 | var expectLastPage = expect(function () { 40 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Marie Curie", "should be on last page"); 41 | test.length($('.reactive-table tbody tr'), 2, "last page should have two rows"); 42 | test.equal($('.reactive-table-navigation .page-number input').val(), "3", "displayed page number should be 3"); 43 | test.length($('.reactive-table-navigation .next-page'), 0, "last page shouldn't have next button"); 44 | 45 | $('.reactive-table-navigation .previous-page').click(); 46 | Meteor.setTimeout(expectBackToSecondPage, 0); 47 | }); 48 | 49 | var expectSecondPage = expect(function () { 50 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Claude Shannon", "should be on the second page"); 51 | test.length($('.reactive-table tbody tr'), 2, "second page should have two rows"); 52 | test.equal($('.reactive-table-navigation .page-number input').val(), "2", "displayed page number should be 2"); 53 | 54 | $('.reactive-table-navigation .next-page').click(); 55 | Meteor.setTimeout(expectLastPage, 0); 56 | }); 57 | 58 | $('.reactive-table-navigation .next-page').click(); 59 | Meteor.setTimeout(expectSecondPage, 0); 60 | }]); 61 | 62 | 63 | Tinytest.add('Pagination - rows per page', function (test) { 64 | testTable( 65 | {collection: rows, settings: {showRowCount: true}}, 66 | function() { 67 | test.equal($('.reactive-table-navigation .rows-per-page .rows-per-page-count').text(), "6"); 68 | }) 69 | }); 70 | 71 | testAsyncMulti('Pagination - rows per page while filtering', [function (test, expect) { 72 | var table = Blaze.renderWithData( 73 | Template.reactiveTable, 74 | {collection: rows, settings: {showRowCount: true}}, 75 | document.body 76 | ); 77 | test.length($('.reactive-table tbody tr'), 6, "initial six rows"); 78 | 79 | var expectTwoRows = expect(function () { 80 | test.length($('.reactive-table tbody tr'), 2, "filtered to two rows"); 81 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "filtered first row"); 82 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Grace Hopper", "filtered second row"); 83 | test.equal($('.reactive-table-navigation .rows-per-page .rows-per-page-count').text(), "2", "Number of rows should be 2"); 84 | Blaze.remove(table); 85 | }); 86 | 87 | $('.reactive-table-filter input').val('g'); 88 | $('.reactive-table-filter input').trigger('input'); 89 | Meteor.setTimeout(expectTwoRows, 1000); 90 | }]); 91 | 92 | testAsyncMulti('Pagination - changing rows per page sets current page to last when past it', [function (test, expect) { 93 | var table = Blaze.renderWithData( 94 | Template.reactiveTable, 95 | {collection: rows, settings: {rowsPerPage: 2}}, 96 | document.body 97 | ); 98 | 99 | var expectFirstPage = expect(function () { 100 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "should be on first page"); 101 | test.length($('.reactive-table tbody tr'), 6, "first page should have six rows"); 102 | test.equal($('.reactive-table-navigation .page-number input').val(), "1", "displayed page number should be 1"); 103 | 104 | Blaze.remove(table); 105 | }); 106 | 107 | var expectSecondPage = expect(function () { 108 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Claude Shannon", "should be on the second page"); 109 | test.length($('.reactive-table tbody tr'), 2, "second page should have two rows"); 110 | test.equal($('.reactive-table-navigation .page-number input').val(), "2", "displayed page number should be 2"); 111 | 112 | $('.reactive-table-navigation .rows-per-page input').val("6"); 113 | $('.reactive-table-navigation .rows-per-page input').trigger("change"); 114 | Meteor.setTimeout(expectFirstPage, 0); 115 | }); 116 | 117 | $('.reactive-table-navigation .next-page').click(); 118 | Meteor.setTimeout(expectSecondPage, 0); 119 | }]); 120 | 121 | testAsyncMulti('Pagination - page input', [function (test, expect) { 122 | var table = Blaze.renderWithData( 123 | Template.reactiveTable, 124 | {collection: rows, settings: {rowsPerPage: 2}}, 125 | document.body 126 | ); 127 | 128 | var expectFirstPage = expect(function () { 129 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "should be on first page"); 130 | test.length($('.reactive-table tbody tr'), 2, "first page should have two rows"); 131 | test.equal($('.reactive-table-navigation .page-number input').val(), "1", "displayed page number should be 1"); 132 | 133 | Blaze.remove(table); 134 | }); 135 | 136 | var expectSecondPage = expect(function () { 137 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Claude Shannon", "should be on the second page"); 138 | test.length($('.reactive-table tbody tr'), 2, "second page should have two rows"); 139 | test.equal($('.reactive-table-navigation .page-number input').val(), "2", "displayed page number should be 2"); 140 | 141 | $('.reactive-table-navigation .page-number input').val("1"); 142 | $('.reactive-table-navigation .page-number input').trigger("change"); 143 | Meteor.setTimeout(expectFirstPage, 0); 144 | }); 145 | 146 | var expectLastPage = expect(function () { 147 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Marie Curie", "should be on last page"); 148 | test.length($('.reactive-table tbody tr'), 2, "last page should have two rows"); 149 | test.equal($('.reactive-table-navigation .page-number input').val(), "3", "displayed page number should be 3"); 150 | 151 | $('.reactive-table-navigation .page-number input').val('2'); 152 | $('.reactive-table-navigation .page-number input').trigger("change"); 153 | Meteor.setTimeout(expectSecondPage, 0); 154 | }); 155 | 156 | $('.reactive-table-navigation .page-number input').val("3"); 157 | $('.reactive-table-navigation .page-number input').trigger("change"); 158 | Meteor.setTimeout(expectLastPage, 0); 159 | }]); 160 | 161 | testAsyncMulti('Pagination - server-side', [function (test, expect) { 162 | var table = Blaze.renderWithData( 163 | Template.reactiveTable, 164 | {collection: 'collection', fields: ['name', 'score'], rowsPerPage: 2}, 165 | document.body 166 | ); 167 | 168 | var expectBackToFirstPage = expect(function () { 169 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "should be on first page"); 170 | test.length($('.reactive-table tbody tr'), 2, "second page should have two rows"); 171 | test.equal($('.reactive-table-navigation .page-number input').val(), "1", "displayed page number should be 1"); 172 | 173 | Blaze.remove(table); 174 | }); 175 | 176 | var expectBackToSecondPage = expect(function () { 177 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Claude Shannon", "should be on the second page"); 178 | test.length($('.reactive-table tbody tr'), 2, "second page should have two rows"); 179 | test.equal($('.reactive-table-navigation .page-number input').val(), "2", "displayed page number should be 2"); 180 | 181 | $('.reactive-table-navigation .previous-page').click(); 182 | Meteor.setTimeout(expectBackToFirstPage, 500); 183 | }); 184 | 185 | var expectLastPage = expect(function () { 186 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Marie Curie", "should be on last page"); 187 | test.length($('.reactive-table tbody tr'), 2, "last page should have two rows"); 188 | test.equal($('.reactive-table-navigation .page-number input').val(), "3", "displayed page number should be 3"); 189 | test.length($('.reactive-table-navigation .next-page'), 0, "last page shouldn't have next button"); 190 | 191 | $('.reactive-table-navigation .previous-page').click(); 192 | Meteor.setTimeout(expectBackToSecondPage, 500); 193 | }); 194 | 195 | var expectSecondPage = expect(function () { 196 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Claude Shannon", "should be on the second page"); 197 | test.length($('.reactive-table tbody tr'), 2, "second page should have two rows"); 198 | test.equal($('.reactive-table-navigation .page-number input').val(), "2", "displayed page number should be 2"); 199 | 200 | $('.reactive-table-navigation .next-page').click(); 201 | Meteor.setTimeout(expectLastPage, 500); 202 | }); 203 | 204 | var expectFirstPage = expect(function () { 205 | test.length($('.reactive-table tbody tr'), 2, "two rows should be rendered"); 206 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "should be on the first page"); 207 | test.equal($('.reactive-table-navigation .page-number input').val(), "1", "displayed page number should be 1"); 208 | test.length($('.reactive-table-navigation .page-number label .page-number-count').text().match(/3/), 1, "displayed page count should be 3"); 209 | test.length($('.reactive-table-navigation .page-number label .page-number-count').text().match(/3/), 1, "displayed page count should be 3"); 210 | test.length($('.reactive-table-navigation .previous-page'), 0, "first page shouldn't have previous button"); 211 | 212 | $('.reactive-table-navigation .next-page').click(); 213 | Meteor.setTimeout(expectSecondPage, 500); 214 | }); 215 | 216 | Meteor.setTimeout(expectFirstPage, 500); 217 | }]); 218 | -------------------------------------------------------------------------------- /test/test_column_toggles.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('Column Toggles - setting', function (test) { 2 | testTable( 3 | { 4 | collection: rows, 5 | settings: { 6 | showColumnToggles: false 7 | } 8 | }, 9 | function () { 10 | test.length($('.reactive-table-columns-dropdown'), 0, "column toggle button should be hidden"); 11 | } 12 | ); 13 | 14 | testTable( 15 | { 16 | collection: rows, 17 | showColumnToggles: true 18 | }, 19 | function () { 20 | test.length($('.reactive-table-columns-dropdown'), 1, "column toggle button should be visible"); 21 | test.length($('.reactive-table th'), 2, "two columns should be displayed"); 22 | test.length($('.reactive-table-columns-dropdown input:checked'), 2, "two checked boxes should be available"); 23 | } 24 | ); 25 | }); 26 | 27 | Tinytest.add('Column Toggles - hidden columns', function (test) { 28 | testTable( 29 | { 30 | collection: rows, 31 | settings: { 32 | showColumnToggles: true, 33 | fields: [ 34 | {key: 'name', label: 'Visible'}, 35 | {key: 'score', label: 'Hidden', hidden: true} 36 | ] 37 | } 38 | }, 39 | function () { 40 | test.length($('.reactive-table-columns-dropdown'), 1, "column toggle button should be visible"); 41 | test.length($('.reactive-table th'), 1, "one column should be displayed"); 42 | test.length($('.reactive-table-columns-dropdown input'), 2, "two checkboxes should be available"); 43 | test.length($('.reactive-table-columns-dropdown input:checked'), 1, "only one box should be checked"); 44 | } 45 | ); 46 | }); 47 | 48 | Tinytest.add('Column Toggles - hidden columns with non-unique keys', function (test) { 49 | /** 50 | * Tests legacy behaviour of duplicate key values without specifying a fieldId 51 | */ 52 | testTable( 53 | { 54 | collection: rows, 55 | settings: { 56 | showColumnToggles: true, 57 | fields: [ 58 | {key: 'name', label: 'Visible'}, 59 | {key: 'name', label: 'Hidden', hidden: true} 60 | ] 61 | } 62 | }, 63 | function () { 64 | test.length($('.reactive-table-columns-dropdown'), 1, "column toggle button should be visible"); 65 | test.length($('.reactive-table th'), 1, "one columns should be displayed"); 66 | test.length($('.reactive-table-columns-dropdown input'), 2, "two checkboxes should be available"); 67 | test.length($('.reactive-table-columns-dropdown input:checked'), 1, "one box should be checked"); 68 | } 69 | ); 70 | 71 | testTable( 72 | { 73 | collection: rows, 74 | settings: { 75 | showColumnToggles: true, 76 | fields: [ 77 | {key: 'name', label: 'Hidden', hidden: true}, 78 | {key: 'name', label: 'Visible'} 79 | ] 80 | } 81 | }, 82 | function () { 83 | test.length($('.reactive-table-columns-dropdown'), 1, "column toggle button should be visible"); 84 | test.length($('.reactive-table th'), 1, "one column should be displayed"); 85 | test.length($('.reactive-table-columns-dropdown input'), 2, "two checkboxes should be available"); 86 | test.length($('.reactive-table-columns-dropdown input:checked'), 1, "one box should be checked"); 87 | } 88 | ); 89 | 90 | }); 91 | 92 | Tinytest.add('Column Toggles - hidden columns with fieldIds', function (test) { 93 | testTable( 94 | { 95 | collection: rows, 96 | settings: { 97 | showColumnToggles: true, 98 | fields: [ 99 | {fieldId: 'one', key: 'name', label: 'Visible'}, 100 | {fieldId: 'two', key: 'score', label: 'Hidden', hidden: true} 101 | ] 102 | } 103 | }, 104 | function () { 105 | test.length($('.reactive-table-columns-dropdown'), 1, "column toggle button should be visible"); 106 | test.length($('.reactive-table th'), 1, "one column should be displayed"); 107 | test.length($('.reactive-table-columns-dropdown input'), 2, "two checkboxes should be available"); 108 | test.length($('.reactive-table-columns-dropdown input:checked'), 1, "only one box should be checked"); 109 | } 110 | ); 111 | }); 112 | 113 | testAsyncMulti('Column Toggles - toggling', [function (test, expect) { 114 | var table = Blaze.renderWithData( 115 | Template.reactiveTable, 116 | { 117 | collection: rows, 118 | showColumnToggles: true, 119 | fields: [ 120 | {key: 'name', label: 'Visible'}, 121 | {key: 'score', label: 'Hidden', hidden: true} 122 | ] 123 | }, 124 | document.body 125 | ); 126 | test.length($('.reactive-table th'), 1, "one column should be displayed"); 127 | test.length($('.reactive-table-columns-dropdown input'), 2, "two checkboxes should be available"); 128 | test.length($('.reactive-table-columns-dropdown input:checked'), 1, "only one box should be checked"); 129 | 130 | var expectSecondColumnOnly = expect(function () { 131 | test.length($('.reactive-table th'), 1, "one column should be displayed"); 132 | test.length($('.reactive-table-columns-dropdown input'), 2, "two checkboxes should be available"); 133 | test.length($('.reactive-table-columns-dropdown input:checked'), 1, "only one box should be checked"); 134 | test.length($('.reactive-table th:first-child').text().trim().match(/Hidden/), 1, "initially hidden column should now be displayed"); 135 | 136 | Blaze.remove(table); 137 | }); 138 | 139 | var expectBothColumns = expect(function () { 140 | test.length($('.reactive-table th'), 2, "two columns should be displayed"); 141 | test.length($('.reactive-table-columns-dropdown input'), 2, "both boxes should be checked"); 142 | test.length($('.reactive-table th:first-child').text().trim().match(/Visible/), 1, "visible column should still be displayed"); 143 | test.length($('.reactive-table th:nth-child(2)').text().trim().match(/Hidden/), 1, "initially hidden column should now be displayed"); 144 | 145 | $('.reactive-table-columns-dropdown input[data-fieldid="0"]').click(); 146 | Meteor.setTimeout(expectSecondColumnOnly, 0); 147 | }); 148 | 149 | $('.reactive-table-columns-dropdown input[data-fieldid="1"]').click(); 150 | Meteor.setTimeout(expectBothColumns, 0); 151 | }]); 152 | 153 | testAsyncMulti('Column Toggles - toggling after adding new columns with fieldId', [function (test, expect) { 154 | var insert2ndField = new ReactiveVar(false); 155 | Template.testReactivity.helpers({ 156 | collection: function () { 157 | return collection; 158 | }, 159 | settings: function () { 160 | if (insert2ndField.get()) { 161 | return { 162 | showColumnToggles: true, 163 | fields: [ 164 | {fieldId: 'one', key: 'name', label: 'Name'}, 165 | {fieldId: 'two', key: 'score', label: 'Score'}, 166 | {fieldId: 'three', key: 'average', label: 'Average'} 167 | ] 168 | } 169 | } else { 170 | return { 171 | showColumnToggles: true, 172 | fields: [ 173 | {fieldId: 'one', key: 'name', label: 'Name'}, 174 | {fieldId: 'three', key: 'average', label: 'Average'} 175 | ] 176 | } 177 | } 178 | } 179 | }); 180 | 181 | var table = Blaze.renderWithData( 182 | Template.testReactivity, 183 | {}, 184 | document.body 185 | ); 186 | 187 | test.length($('.reactive-table th'), 2, "two columns should be displayed"); 188 | test.length($('.reactive-table-columns-dropdown input'), 2, "two checkboxes should be available"); 189 | test.length($('.reactive-table-columns-dropdown input:checked'), 2, "both boxes should be checked"); 190 | 191 | var expectColumnsOneAndTwoOnly = expect(function () { 192 | test.length($('.reactive-table th'), 2, "two columns should be displayed"); 193 | test.length($('.reactive-table-columns-dropdown input'), 3, "three checkboxes should be available"); 194 | test.length($('.reactive-table-columns-dropdown input:checked'), 2, "two checkboxes box should be checked"); 195 | test.length($('.reactive-table-columns-dropdown input:checked[data-fieldid="two"]'), 1, "newly inserted field should be checked"); 196 | test.length($('.reactive-table-columns-dropdown input:checked[data-fieldid="three"]'), 0, "previously hidden field should still be unchecked"); 197 | test.length($('.reactive-table th:first-child').text().trim().match(/Name/), 1, "Name column should still be visible"); 198 | test.length($('.reactive-table th:nth-child(2)').text().trim().match(/Score/), 1, "Score column should now be visible"); 199 | 200 | Blaze.remove(table); 201 | }); 202 | 203 | var expectOneColumnOnly = expect(function () { 204 | test.length($('.reactive-table th'), 1, "one columns should be displayed"); 205 | test.length($('.reactive-table-columns-dropdown input:checked'), 1, "one box should be checked"); 206 | test.length($('.reactive-table th:first-child').text().trim().match(/Name/), 1, "visible column should still be displayed"); 207 | 208 | insert2ndField.set(true); 209 | Meteor.setTimeout(expectColumnsOneAndTwoOnly, 0); 210 | }); 211 | 212 | $('.reactive-table-columns-dropdown input[data-fieldid="three"]').click(); 213 | Meteor.setTimeout(expectOneColumnOnly, 0); 214 | }]); 215 | 216 | testAsyncMulti('Column Toggles - toggling after adding new columns without fieldIds', [function (test, expect) { 217 | var insert2ndField = new ReactiveVar(false); 218 | Template.testReactivity.helpers({ 219 | collection: function () { 220 | return collection; 221 | }, 222 | settings: function () { 223 | if (insert2ndField.get()) { 224 | return { 225 | showColumnToggles: true, 226 | fields: [ 227 | {key: 'name', label: 'Name'}, 228 | {key: 'score', label: 'Score'}, 229 | {key: 'average', label: 'Average'} 230 | ] 231 | } 232 | } else { 233 | return { 234 | showColumnToggles: true, 235 | fields: [ 236 | {key: 'name', label: 'Name'}, 237 | {key: 'average', label: 'Average'} 238 | ] 239 | } 240 | } 241 | } 242 | }); 243 | 244 | var table = Blaze.renderWithData( 245 | Template.testReactivity, 246 | {}, 247 | document.body 248 | ); 249 | 250 | test.length($('.reactive-table th'), 2, "two columns should be displayed"); 251 | test.length($('.reactive-table-columns-dropdown input'), 2, "two checkboxes should be available"); 252 | test.length($('.reactive-table-columns-dropdown input:checked'), 2, "both boxes should be checked"); 253 | 254 | var expectColumnsOneAndTwoOnly = expect(function () { 255 | test.length($('.reactive-table th'), 2, "two columns should be displayed"); 256 | test.length($('.reactive-table-columns-dropdown input'), 3, "three checkboxes should be available"); 257 | test.length($('.reactive-table-columns-dropdown input:checked'), 2, "two checkboxes box should be checked"); 258 | test.length($('.reactive-table th:first-child').text().trim().match(/Name/), 1, "Name column should still be visible"); 259 | 260 | // Tests for known erroneous behaviour in this case if fieldIds are not explicitly set: 261 | test.length($('.reactive-table-columns-dropdown input:checked[data-fieldid="1"]'), 0, "newly inserted field will be unchecked"); 262 | test.length($('.reactive-table th:nth-child(2)').text().trim().match(/Average/), 1, "Average column will still be visible"); 263 | 264 | Blaze.remove(table); 265 | }); 266 | 267 | var expectOneColumnOnly = expect(function () { 268 | test.length($('.reactive-table th'), 1, "one columns should be displayed"); 269 | test.length($('.reactive-table-columns-dropdown input:checked'), 1, "one box should be checked"); 270 | test.length($('.reactive-table th:first-child').text().trim().match(/Name/), 1, "visible column should still be displayed"); 271 | 272 | insert2ndField.set(true); 273 | Meteor.setTimeout(expectColumnsOneAndTwoOnly, 0); 274 | }); 275 | 276 | $('.reactive-table-columns-dropdown input[data-fieldid="1"]').click(); 277 | Meteor.setTimeout(expectOneColumnOnly, 0); 278 | }]); 279 | 280 | Tinytest.add('Column Toggles - hideToggle option', function (test) { 281 | testTable( 282 | { 283 | collection: rows, 284 | settings: { 285 | showColumnToggles: true, 286 | fields: [ 287 | {key: 'name', hideToggle: false}, 288 | {key: 'score', hideToggle: true} 289 | ] 290 | } 291 | }, 292 | function () { 293 | test.length($('.reactive-table-columns-dropdown'), 1, "column toggle button should be visible"); 294 | test.length($('.reactive-table th'), 2, "two columns should be displayed"); 295 | test.length($('.reactive-table-columns-dropdown input'), 1, "one checkbox should be available"); 296 | test.length($('.reactive-table-columns-dropdown input:checked'), 1, "one box should be checked"); 297 | } 298 | ); 299 | }); 300 | -------------------------------------------------------------------------------- /test/test_reactivity.js: -------------------------------------------------------------------------------- 1 | testAsyncMulti('Reactivity - collection', [function (test, expect) { 2 | var collection = new Mongo.Collection(); 3 | collection.insert({name: 'item 1', value: 1}); 4 | var table = Blaze.renderWithData( 5 | Template.reactiveTable, 6 | {collection: collection}, 7 | document.body 8 | ); 9 | test.length($('.reactive-table tbody tr'), 1, "table should initially have one row"); 10 | collection.insert({name: 'item 2', value: 2}); 11 | test.length($('.reactive-table tbody tr'), 2, "table should reactively add second row"); 12 | collection.remove({name: 'item 2'}); 13 | test.length($('.reactive-table tbody tr'), 1, "table should reactively remove a row"); 14 | 15 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "1", "table row should have the initial value"); 16 | collection.update({name: 'item 1'}, {'$set': {value: 2}}); 17 | Meteor.setTimeout(expect(function () { 18 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "2", "table should reactively update with new value"); 19 | Blaze.remove(table); 20 | }), 0); 21 | }]); 22 | 23 | testAsyncMulti('Reactivity - cursor', [function (test, expect) { 24 | var collection = new Meteor.Collection(); 25 | collection.insert({name: 'item 1', value: 1}); 26 | var table = Blaze.renderWithData( 27 | Template.reactiveTable, 28 | {collection: collection.find()}, 29 | document.body 30 | ); 31 | test.length($('.reactive-table tbody tr'), 1, "table should initially have one row"); 32 | collection.insert({name: 'item 2', value: 2}); 33 | test.length($('.reactive-table tbody tr'), 2, "table should reactively add second row"); 34 | collection.remove({name: 'item 2'}); 35 | test.length($('.reactive-table tbody tr'), 1, "table should reactively remove a row"); 36 | 37 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "1", "table row should have the initial value"); 38 | collection.update({name: 'item 1'}, {'$set': {value: 2}}); 39 | Meteor.setTimeout(expect(function () { 40 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "2", "table should reactively update with new value"); 41 | Blaze.remove(table); 42 | }), 0); 43 | }]); 44 | 45 | testAsyncMulti('Reactivity - collection changed by helper', [function (test, expect) { 46 | var collection = new Mongo.Collection(); 47 | collection.insert({name: 'item 1', value: 1}); 48 | var showCollection = new ReactiveVar(false); 49 | 50 | Template.testReactivity.helpers({ 51 | collection: function () { 52 | if (showCollection.get()) { 53 | return collection; 54 | } else { 55 | return []; 56 | } 57 | } 58 | }); 59 | 60 | var view = Blaze.renderWithData( 61 | Template.testReactivity, 62 | {}, 63 | document.body 64 | ); 65 | 66 | test.length($('.reactive-table tbody tr'), 0, 'table should be empty'); 67 | 68 | showCollection.set(true); 69 | Meteor.setTimeout(expect(function () { 70 | test.length($('.reactive-table tbody tr'), 1, 'table should render collection'); 71 | Blaze.remove(view); 72 | }), 0); 73 | }]); 74 | 75 | testAsyncMulti('Reactivity - setting changed by helper', [function (test, expect) { 76 | var useFontAwesome = new ReactiveVar(false); 77 | Template.testReactivity.helpers({ 78 | collection: function () { 79 | return collection; 80 | }, 81 | settings: function () { 82 | if (useFontAwesome.get()) { 83 | return {useFontAwesome: true} 84 | } else { 85 | return {useFontAwesome: false}; 86 | } 87 | } 88 | }); 89 | 90 | var view = Blaze.renderWithData( 91 | Template.testReactivity, 92 | {}, 93 | document.body 94 | ); 95 | 96 | test.length($('.reactive-table-filter .fa'), 0, "font awesome should be off"); 97 | 98 | useFontAwesome.set(true); 99 | Meteor.setTimeout(expect(function () { 100 | test.length($('.reactive-table-filter .fa'), 1, "font awesome should be on"); 101 | Blaze.remove(view); 102 | }), 0); 103 | }]); 104 | 105 | testAsyncMulti('Reactivity - user-editable setting persists', [function (test, expect) { 106 | var rowsPerPage = new ReactiveVar(2); 107 | Template.testReactivity.helpers({ 108 | collection: function () { 109 | return collection; 110 | }, 111 | settings: function () { 112 | return {rowsPerPage: rowsPerPage.get()}; 113 | } 114 | }); 115 | 116 | var view = Blaze.renderWithData( 117 | Template.testReactivity, 118 | {}, 119 | document.body 120 | ); 121 | 122 | test.length($('.reactive-table tbody tr'), 2, "two rows should be rendered"); 123 | 124 | var expectSameRowsPerPage = expect(function () { 125 | test.length($('.reactive-table tbody tr'), 2, "two rows should be rendered"); 126 | Blaze.remove(view); 127 | }); 128 | 129 | rowsPerPage.set(5); 130 | Meteor.setTimeout(expectSameRowsPerPage, 0); 131 | }]); 132 | 133 | testAsyncMulti('Reactivity - user setting persists when other arguments change', [function (test, expect) { 134 | var showCollection = new ReactiveVar(true); 135 | Template.testReactivity.helpers({ 136 | collection: function () { 137 | if (showCollection.get()) { 138 | return collection; 139 | } else { 140 | return rows; 141 | } 142 | }, 143 | settings: function () { 144 | return {rowsPerPage: 2}; 145 | } 146 | }); 147 | 148 | var view = Blaze.renderWithData( 149 | Template.testReactivity, 150 | {}, 151 | document.body 152 | ); 153 | 154 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "should be on first page"); 155 | test.length($('.reactive-table tbody tr'), 2, "first page should have two rows"); 156 | test.equal($('.reactive-table-navigation .page-number input').val(), "1", "displayed page number should be 1"); 157 | 158 | var testSecondPage = function () { 159 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Claude Shannon", "should be on the second page"); 160 | test.equal($('.reactive-table-navigation .page-number input').val(), "2", "displayed page number should be 2"); 161 | }; 162 | 163 | var expectStillSecondPage = expect(function () { 164 | testSecondPage(); 165 | Blaze.remove(view); 166 | }); 167 | 168 | var expectSecondPage = expect(function () { 169 | testSecondPage(); 170 | 171 | showCollection.set(false); 172 | Meteor.setTimeout(expectStillSecondPage, 0); 173 | }); 174 | 175 | $('.reactive-table-navigation .page-number input').val('2'); 176 | $('.reactive-table-navigation .page-number input').trigger("change"); 177 | 178 | Meteor.setTimeout(expectSecondPage, 0); 179 | 180 | 181 | showCollection.set(true); 182 | }]); 183 | 184 | testAsyncMulti('Reactivity - adding new visible field with fieldIds', [function (test, expect) { 185 | var add2ndField = new ReactiveVar(false); 186 | Template.testReactivity.helpers({ 187 | collection: function () { 188 | return collection; 189 | }, 190 | settings: function () { 191 | if (add2ndField.get()) { 192 | return { 193 | fields: [ 194 | {fieldId: 'one', key: 'name', label: 'name', hidden: false}, 195 | {fieldId: 'two', key: 'score', label: 'score', hidden: false} 196 | ] 197 | } 198 | } else { 199 | return { 200 | fields: [ 201 | {fieldId: 'one', key: 'name', label: 'name', hidden: false} 202 | ] 203 | } 204 | } 205 | } 206 | }); 207 | 208 | var view = Blaze.renderWithData( 209 | Template.testReactivity, 210 | {}, 211 | document.body 212 | ); 213 | 214 | test.length($('.reactive-table th'), 1, "one column should be rendered"); 215 | test.length($('.reactive-table th:first-child').text().trim().match(/^name/), 1, "first column should be name"); 216 | 217 | add2ndField.set(true); 218 | Meteor.setTimeout(expect(function () { 219 | test.length($('.reactive-table th'), 2, "two columns should be rendered"); 220 | test.length($('.reactive-table th:first-child').text().trim().match(/^name/), 1, "first column should be name"); 221 | test.length($('.reactive-table th:nth-child(2)').text().trim().match(/^score/), 1, "second column should be score"); 222 | Blaze.remove(view); 223 | }), 0); 224 | }]); 225 | 226 | testAsyncMulti('Reactivity - adding new hidden field with fieldIds', [function (test, expect) { 227 | var add2ndField = new ReactiveVar(false); 228 | Template.testReactivity.helpers({ 229 | collection: function () { 230 | return collection; 231 | }, 232 | settings: function () { 233 | if (add2ndField.get()) { 234 | return { 235 | fields: [ 236 | {fieldId: 'one', key: 'name', label: 'name', hidden: false}, 237 | {fieldId: 'two', key: 'score', label: 'score', hidden: true} 238 | ] 239 | } 240 | } else { 241 | return { 242 | fields: [ 243 | {fieldId: 'one', key: 'name', label: 'name', hidden: false} 244 | ] 245 | } 246 | } 247 | } 248 | }); 249 | 250 | var view = Blaze.renderWithData( 251 | Template.testReactivity, 252 | {}, 253 | document.body 254 | ); 255 | 256 | test.length($('.reactive-table th'), 1, "one column should be rendered"); 257 | test.length($('.reactive-table th:first-child').text().trim().match(/^name/), 1, "first column should be name"); 258 | 259 | add2ndField.set(true); 260 | Meteor.setTimeout(expect(function () { 261 | test.length($('.reactive-table th'), 1, "one column should be rendered"); 262 | test.length($('.reactive-table th:first-child').text().trim().match(/^name/), 1, "first column should be name"); 263 | test.isNull($('.reactive-table th:nth-child(2)').text().trim().match(/^score/), 1, "second column should be hidden"); 264 | Blaze.remove(view); 265 | }), 0); 266 | }]); 267 | 268 | testAsyncMulti('Reactivity - adding new visible field without fieldIds', [function (test, expect) { 269 | var add2ndField = new ReactiveVar(false); 270 | Template.testReactivity.helpers({ 271 | collection: function () { 272 | return collection; 273 | }, 274 | settings: function () { 275 | if (add2ndField.get()) { 276 | return { 277 | fields: [ 278 | {key: 'score', label: 'score', hidden: false}, 279 | {key: 'name', label: 'name', hidden: false} 280 | ] 281 | } 282 | } else { 283 | return { 284 | fields: [ 285 | {key: 'name', label: 'name', hidden: false} 286 | ] 287 | } 288 | } 289 | } 290 | }); 291 | 292 | var view = Blaze.renderWithData( 293 | Template.testReactivity, 294 | {}, 295 | document.body 296 | ); 297 | 298 | test.length($('.reactive-table th'), 1, "one column should be rendered"); 299 | test.length($('.reactive-table th:first-child').text().trim().match(/^name/), 1, "first column should be name"); 300 | 301 | add2ndField.set(true); 302 | Meteor.setTimeout(expect(function () { 303 | test.length($('.reactive-table th'), 2, "two columns should be rendered"); 304 | test.length($('.reactive-table th:first-child').text().trim().match(/^score/), 1, "first column should be score"); 305 | test.length($('.reactive-table th:nth-child(2)').text().trim().match(/^name/), 1, "second column should be name"); 306 | Blaze.remove(view); 307 | }), 0); 308 | }]); 309 | 310 | testAsyncMulti('Reactivity - server-side collection', [function (test, expect) { 311 | var table = Blaze.renderWithData( 312 | Template.reactiveTable, 313 | {collection: 'reactivity-test', fields: ['name', 'value']}, 314 | document.body 315 | ); 316 | 317 | var expectUpdate = expect(function () { 318 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "2", "table should reactively update with new value"); 319 | Blaze.remove(table); 320 | }); 321 | 322 | var expectRemove = expect(function () { 323 | test.length($('.reactive-table tbody tr'), 1, "table should reactively remove a row"); 324 | 325 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "1", "table row should have the initial value"); 326 | Meteor.call('testUpdate', function () { 327 | Meteor.setTimeout(expectUpdate, 500); 328 | }); 329 | }); 330 | 331 | var expectInsert = expect(function () { 332 | test.length($('.reactive-table tbody tr'), 2, "table should reactively add second row"); 333 | Meteor.call('testRemove', function () { 334 | Meteor.setTimeout(expectRemove, 500); 335 | }); 336 | }); 337 | 338 | var expectInitialRow = expect(function () { 339 | test.length($('.reactive-table tbody tr'), 1, "table should initially have one row"); 340 | Meteor.call('testInsert', function () { 341 | Meteor.setTimeout(expectInsert, 500); 342 | }); 343 | }); 344 | 345 | Meteor.setTimeout(expectInitialRow, 500); 346 | }]); 347 | 348 | testAsyncMulti('Reactivity - server-side collection access', [function (test, expect) { 349 | var table = Blaze.renderWithData( 350 | Template.reactiveTable, 351 | {collection: 'reactivity-test-access', fields: ['name', 'value']}, 352 | document.body 353 | ); 354 | 355 | var expectDataHidden = expect(function () { 356 | test.length($('.reactive-table tbody tr'), 0, "table should remove row"); 357 | Blaze.remove(table); 358 | }); 359 | 360 | var expectData = expect(function () { 361 | test.length($('.reactive-table tbody tr'), 1, "table should show one row"); 362 | Meteor.logout(function () { 363 | Meteor.setTimeout(expectDataHidden, 500); 364 | }); 365 | }); 366 | 367 | var expectNoData = expect(function () { 368 | test.length($('.reactive-table tbody tr'), 0, "table should initially have no rows"); 369 | Meteor.loginWithPassword('abcd', 'abcd1234', function () { 370 | Meteor.setTimeout(expectData, 500); 371 | }); 372 | }); 373 | 374 | Meteor.logout(function () { 375 | Meteor.setTimeout(expectNoData, 500); 376 | }); 377 | }]); 378 | 379 | -------------------------------------------------------------------------------- /test/test_filtering.js: -------------------------------------------------------------------------------- 1 | testAsyncMulti('Filtering - one input', [function (test, expect) { 2 | var table = Blaze.renderWithData( 3 | Template.reactiveTable, 4 | {collection: rows}, 5 | document.body 6 | ); 7 | test.length($('.reactive-table tbody tr'), 6, "initial six rows"); 8 | 9 | var expectTwoRows = expect(function () { 10 | test.length($('.reactive-table tbody tr'), 2, "filtered to two rows"); 11 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "filtered first row"); 12 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Grace Hopper", "filtered second row"); 13 | Blaze.remove(table); 14 | }); 15 | 16 | $('.reactive-table-filter input').val('g'); 17 | $('.reactive-table-filter input').trigger('input'); 18 | Meteor.setTimeout(expectTwoRows, 1000); 19 | }]); 20 | 21 | testAsyncMulti('Filtering - two inputs', [function (test, expect) { 22 | var table = Blaze.renderWithData( 23 | Template.reactiveTable, 24 | {collection: rows}, 25 | document.body 26 | ); 27 | test.length($('.reactive-table tbody tr'), 6, "initial six rows"); 28 | 29 | var expectOneRow = expect(function () { 30 | test.length($('.reactive-table tbody tr'), 1, "filtered to one row"); 31 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Grace Hopper", "filtered row"); 32 | Blaze.remove(table); 33 | }); 34 | 35 | $('.reactive-table-filter input').val('g hopper'); 36 | $('.reactive-table-filter input').trigger('input'); 37 | Meteor.setTimeout(expectOneRow, 1000); 38 | }]); 39 | 40 | testAsyncMulti('Filtering - quotes', [function (test, expect) { 41 | var table = Blaze.renderWithData( 42 | Template.reactiveTable, 43 | {collection: rows}, 44 | document.body 45 | ); 46 | test.length($('.reactive-table tbody tr'), 6, "initial six rows"); 47 | 48 | var expectZeroRows = expect(function () { 49 | test.length($('.reactive-table tbody tr'), 0, "filtered to zero rows"); 50 | Blaze.remove(table); 51 | }); 52 | 53 | var expectOneRow = expect(function () { 54 | test.length($('.reactive-table tbody tr'), 1, "filtered to one row"); 55 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Grace Hopper", "filtered first row"); 56 | 57 | $('.reactive-table-filter input').val("\"hopper grace\""); 58 | $('.reactive-table-filter input').trigger('input'); 59 | Meteor.setTimeout(expectZeroRows, 1000); 60 | }); 61 | 62 | $('.reactive-table-filter input').val("\"grace hopper\""); 63 | $('.reactive-table-filter input').trigger('input'); 64 | Meteor.setTimeout(expectOneRow, 1000); 65 | }]); 66 | 67 | testAsyncMulti('Filtering - numerical', [function (test, expect) { 68 | var table = Blaze.renderWithData( 69 | Template.reactiveTable, 70 | {collection: rows}, 71 | document.body 72 | ); 73 | test.length($('.reactive-table tbody tr'), 6, "initial six rows"); 74 | 75 | var expectThreeRows = expect(function () { 76 | test.length($('.reactive-table tbody tr'), 3, "filtered to three rows"); 77 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "filtered first row"); 78 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Claude Shannon", "filtered second row"); 79 | Blaze.remove(table); 80 | }); 81 | 82 | $('.reactive-table-filter input').val("5"); 83 | $('.reactive-table-filter input').trigger('input'); 84 | Meteor.setTimeout(expectThreeRows, 1000); 85 | }]); 86 | 87 | testAsyncMulti('Filtering - numbers in strings', [function (test, expect) { 88 | var numberStringRows = _.map(rows, function (row) { 89 | var newRow = {name: row.name, score: row.score + "points"}; 90 | return newRow; 91 | }); 92 | var table = Blaze.renderWithData( 93 | Template.reactiveTable, 94 | {collection: numberStringRows}, 95 | document.body 96 | ); 97 | test.length($('.reactive-table tbody tr'), 6, "initial six rows"); 98 | 99 | var expectThreeRows = expect(function () { 100 | test.length($('.reactive-table tbody tr'), 4, "filtered to four rows"); 101 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "filtered first row"); 102 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Claude Shannon", "filtered second row"); 103 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Nikola Tesla", "filtered fourth row"); 104 | Blaze.remove(table); 105 | }); 106 | 107 | $('.reactive-table-filter input').val("5"); 108 | $('.reactive-table-filter input').trigger('input'); 109 | Meteor.setTimeout(expectThreeRows, 1000); 110 | }]); 111 | 112 | testAsyncMulti('Filtering - server-side', [function (test, expect) { 113 | var table = Blaze.renderWithData( 114 | Template.reactiveTable, 115 | {collection: 'collection', fields: ['name', 'score']}, 116 | document.body 117 | ); 118 | 119 | var expectTwoRows = expect(function () { 120 | test.length($('.reactive-table tbody tr'), 2, "filtered to two rows"); 121 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "filtered first row"); 122 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Grace Hopper", "filtered second row"); 123 | Blaze.remove(table); 124 | }); 125 | 126 | var expectSixRows = expect(function () { 127 | test.length($('.reactive-table tbody tr'), 6, "initial 6 rows"); 128 | 129 | $('.reactive-table-filter input').val('g'); 130 | $('.reactive-table-filter input').trigger('input'); 131 | Meteor.setTimeout(expectTwoRows, 1000); 132 | }); 133 | 134 | Meteor.setTimeout(expectSixRows, 500); 135 | }]); 136 | 137 | testAsyncMulti('Filtering - enableRegex false client side', [function (test, expect) { 138 | var rows = [ 139 | {name: 'item 1', value: '1+2'}, 140 | {name: 'item 2', value: 'abc'} 141 | ]; 142 | 143 | var table = Blaze.renderWithData( 144 | Template.reactiveTable, 145 | {collection: rows}, 146 | document.body 147 | ); 148 | test.length($('.reactive-table tbody tr'), 2, "initial two rows"); 149 | 150 | var expectOneRow = expect(function () { 151 | test.length($('.reactive-table tbody tr'), 1, "filtered to one row"); 152 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "filtered first row"); 153 | Blaze.remove(table); 154 | }); 155 | 156 | $('.reactive-table-filter input').val('+'); 157 | $('.reactive-table-filter input').trigger('input'); 158 | Meteor.setTimeout(expectOneRow, 1000); 159 | }]); 160 | 161 | testAsyncMulti('Filtering - enableRegex true client side', [function (test, expect) { 162 | var rows = [ 163 | {name: 'item 1', value: '1+2'}, 164 | {name: 'item 2', value: 'abc'} 165 | ]; 166 | 167 | var table = Blaze.renderWithData( 168 | Template.reactiveTable, 169 | {collection: rows, enableRegex: true}, 170 | document.body 171 | ); 172 | test.length($('.reactive-table tbody tr'), 2, "initial two rows"); 173 | 174 | var expectTwoRows = expect(function () { 175 | test.length($('.reactive-table tbody tr'), 2, "filtered to both rows"); 176 | Blaze.remove(table); 177 | }); 178 | 179 | var expectOneRow = expect(function () { 180 | test.length($('.reactive-table tbody tr'), 1, "filtered to one row"); 181 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "filtered first row"); 182 | 183 | $('.reactive-table-filter input').val('a|1'); 184 | $('.reactive-table-filter input').trigger('input'); 185 | Meteor.setTimeout(expectTwoRows, 1000); 186 | }); 187 | 188 | $('.reactive-table-filter input').val('\\+'); 189 | $('.reactive-table-filter input').trigger('input'); 190 | Meteor.setTimeout(expectOneRow, 1000); 191 | }]); 192 | 193 | testAsyncMulti('Filtering - enableRegex false server side', [function (test, expect) { 194 | var table = Blaze.renderWithData( 195 | Template.reactiveTable, 196 | {collection: 'filter-regex-disabled', fields: ['name', 'value']}, 197 | document.body 198 | ); 199 | 200 | var expectOneRow = expect(function () { 201 | test.length($('.reactive-table tbody tr'), 1, "filtered to one row"); 202 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "filtered first row"); 203 | Blaze.remove(table); 204 | }); 205 | 206 | var expectInitialRows = expect(function () { 207 | test.length($('.reactive-table tbody tr'), 2, "initial two rows"); 208 | 209 | $('.reactive-table-filter input').val('+'); 210 | $('.reactive-table-filter input').trigger('input'); 211 | Meteor.setTimeout(expectOneRow, 1000); 212 | }); 213 | 214 | Meteor.setTimeout(expectInitialRows, 500); 215 | }]); 216 | 217 | testAsyncMulti('Filtering - enableRegex true server side', [function (test, expect) { 218 | var table = Blaze.renderWithData( 219 | Template.reactiveTable, 220 | {collection: 'filter-regex-enabled', fields: ['name', 'value']}, 221 | document.body 222 | ); 223 | 224 | var expectTwoRows = expect(function () { 225 | test.length($('.reactive-table tbody tr'), 2, "filtered to both rows"); 226 | Blaze.remove(table); 227 | }); 228 | 229 | var expectOneRow = expect(function () { 230 | test.length($('.reactive-table tbody tr'), 1, "filtered to one row"); 231 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "filtered first row"); 232 | 233 | $('.reactive-table-filter input').val('a|1'); 234 | $('.reactive-table-filter input').trigger('input'); 235 | Meteor.setTimeout(expectTwoRows, 1000); 236 | }); 237 | 238 | var expectInitialRows = expect(function () { 239 | test.length($('.reactive-table tbody tr'), 2, "initial two rows"); 240 | 241 | $('.reactive-table-filter input').val('\\+'); 242 | $('.reactive-table-filter input').trigger('input'); 243 | Meteor.setTimeout(expectOneRow, 1000); 244 | }); 245 | 246 | Meteor.setTimeout(expectInitialRows, 500); 247 | }]); 248 | 249 | testAsyncMulti('Filtering - server-side field inclusion', [function (test, expect) { 250 | var table = Blaze.renderWithData( 251 | Template.reactiveTable, 252 | {collection: 'filter-inclusion', fields: ['name', 'value']}, 253 | document.body 254 | ); 255 | 256 | var expectNoRows = expect(function () { 257 | test.length($('.reactive-table tbody tr'), 0, "no rows should match"); 258 | Blaze.remove(table); 259 | }); 260 | 261 | var expectTwoRowsNoValues = expect(function () { 262 | test.length($('.reactive-table tbody tr'), 2, "initial two rows"); 263 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "first column content"); 264 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "", "second column content"); 265 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "item 2", "first column content"); 266 | test.equal($('.reactive-table tbody tr:nth-child(2) td:nth-child(2)').text(), "", "second column content"); 267 | 268 | $('.reactive-table-filter input').val('abc'); 269 | $('.reactive-table-filter input').trigger('input'); 270 | Meteor.setTimeout(expectNoRows, 1000); 271 | }); 272 | 273 | Meteor.setTimeout(expectTwoRowsNoValues, 500); 274 | }]); 275 | 276 | testAsyncMulti('Filtering - server-side field exclusion', [function (test, expect) { 277 | var table = Blaze.renderWithData( 278 | Template.reactiveTable, 279 | {collection: 'filter-exclusion', fields: ['name', 'value']}, 280 | document.body 281 | ); 282 | 283 | var expectNoRows = expect(function () { 284 | test.length($('.reactive-table tbody tr'), 0, "no rows should match"); 285 | Blaze.remove(table); 286 | }); 287 | 288 | var expectTwoRowsNoValues = expect(function () { 289 | test.length($('.reactive-table tbody tr'), 2, "initial two rows"); 290 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "first column content"); 291 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "", "second column content"); 292 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "item 2", "first column content"); 293 | test.equal($('.reactive-table tbody tr:nth-child(2) td:nth-child(2)').text(), "", "second column content"); 294 | 295 | $('.reactive-table-filter input').val('abc'); 296 | $('.reactive-table-filter input').trigger('input'); 297 | Meteor.setTimeout(expectNoRows, 1000); 298 | }); 299 | 300 | Meteor.setTimeout(expectTwoRowsNoValues, 500); 301 | }]); 302 | 303 | testAsyncMulti('Filtering - server-side nested field inclusion', [function (test, expect) { 304 | var table = Blaze.renderWithData( 305 | Template.reactiveTable, 306 | {collection: 'nested-filter-inclusion', fields: ['name', 'nested.0.value']}, 307 | document.body 308 | ); 309 | 310 | var expectOneRow = expect(function () { 311 | test.length($('.reactive-table tbody tr'), 1, "one row should match"); 312 | Blaze.remove(table); 313 | }); 314 | 315 | var expectNoRows = expect(function () { 316 | test.length($('.reactive-table tbody tr'), 0, "no rows should match"); 317 | 318 | $('.reactive-table-filter input').val('2'); 319 | $('.reactive-table-filter input').trigger('input'); 320 | Meteor.setTimeout(expectOneRow, 1000); 321 | }); 322 | 323 | var expectTwoRowsNestedValues = expect(function () { 324 | test.length($('.reactive-table tbody tr'), 2, "initial two rows"); 325 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "", "first column content"); 326 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "value 1", "second column content"); 327 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "", "first column content"); 328 | test.equal($('.reactive-table tbody tr:nth-child(2) td:nth-child(2)').text(), "value 2", "second column content"); 329 | 330 | $('.reactive-table-filter input').val('item'); 331 | $('.reactive-table-filter input').trigger('input'); 332 | Meteor.setTimeout(expectNoRows, 1000); 333 | }); 334 | 335 | Meteor.setTimeout(expectTwoRowsNestedValues, 500); 336 | }]); 337 | 338 | testAsyncMulti('Filtering - server-side nested field exclusion', [function (test, expect) { 339 | var table = Blaze.renderWithData( 340 | Template.reactiveTable, 341 | {collection: 'nested-filter-exclusion', fields: ['name', 'nested.0.value']}, 342 | document.body 343 | ); 344 | 345 | var expectNoRows = expect(function () { 346 | test.length($('.reactive-table tbody tr'), 0, "no rows should match"); 347 | Blaze.remove(table); 348 | }); 349 | 350 | var expectTwoRowsNoValues = expect(function () { 351 | test.length($('.reactive-table tbody tr'), 2, "initial two rows"); 352 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "item 1", "first column content"); 353 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "", "second column content"); 354 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "item 2", "first column content"); 355 | test.equal($('.reactive-table tbody tr:nth-child(2) td:nth-child(2)').text(), "", "second column content"); 356 | 357 | $('.reactive-table-filter input').val('value'); 358 | $('.reactive-table-filter input').trigger('input'); 359 | Meteor.setTimeout(expectNoRows, 1000); 360 | }); 361 | 362 | Meteor.setTimeout(expectTwoRowsNoValues, 500); 363 | }]); 364 | -------------------------------------------------------------------------------- /test/test_sorting.js: -------------------------------------------------------------------------------- 1 | testAsyncMulti('Sorting - direction', [function (test, expect) { 2 | var table = Blaze.renderWithData( 3 | Template.reactiveTable, 4 | {collection: rows}, 5 | document.body 6 | ); 7 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "initial first row"); 8 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Carl Friedrich Gauss", "initial second row"); 9 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Grace Hopper", "initial fourth row"); 10 | 11 | var expectAscending = expect(function () { 12 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "ascending first row"); 13 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Carl Friedrich Gauss", "ascending second row"); 14 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Grace Hopper", "ascending fourth row"); 15 | Blaze.remove(table); 16 | }); 17 | 18 | var expectDescending = expect(function () { 19 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "descending first row"); 20 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Marie Curie", "descending second row"); 21 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "descending fourth row"); 22 | 23 | $('.reactive-table th:first-child').click(); 24 | Meteor.setTimeout(expectAscending, 0); 25 | }); 26 | 27 | $('.reactive-table th:first-child').click(); 28 | Meteor.setTimeout(expectDescending, 0); 29 | }]); 30 | 31 | testAsyncMulti('Sorting - column', [function (test, expect) { 32 | var table = Blaze.renderWithData( 33 | Template.reactiveTable, 34 | {collection: collection}, 35 | document.body 36 | ); 37 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "initial first row"); 38 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Carl Friedrich Gauss", "initial second row"); 39 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Grace Hopper", "initial fourth row"); 40 | 41 | var expectSecondColumnDescending = expect(function () { 42 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "2nd column descending first row"); 43 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Grace Hopper", "2nd column descending second row"); 44 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Marie Curie", "2nd column descending fourth row"); 45 | Blaze.remove(table); 46 | }); 47 | 48 | var expectSecondColumn = expect(function () { 49 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "2nd column first row"); 50 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Ada Lovelace", "2nd column second row"); 51 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "2nd column fourth row"); 52 | 53 | $('.reactive-table th:nth-child(2)').click(); 54 | Meteor.setTimeout(expectSecondColumnDescending, 0); 55 | }); 56 | 57 | $('.reactive-table th:nth-child(2)').click(); 58 | Meteor.setTimeout(expectSecondColumn, 0); 59 | }]); 60 | 61 | testAsyncMulti('Sorting - after adding new column with fieldIds', [function (test, expect) { 62 | var insert1stField = new ReactiveVar(false); 63 | Template.testReactivity.helpers({ 64 | collection: function () { 65 | return collection; 66 | }, 67 | settings: function () { 68 | if (insert1stField.get()) { 69 | return { 70 | fields: [ 71 | {fieldId: 'name', key: 'name', label: 'Name'}, 72 | {fieldId: 'score', key: 'score', label: 'Score'} 73 | ] 74 | } 75 | } else { 76 | return { 77 | fields: [ 78 | {fieldId: 'score', key: 'score', label: 'Score'}, 79 | ] 80 | } 81 | } 82 | } 83 | }); 84 | 85 | var table = Blaze.renderWithData( 86 | Template.testReactivity, 87 | {}, 88 | document.body 89 | ); 90 | 91 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "0", "initial first row"); 92 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "5", "initial second row"); 93 | test.equal($('.reactive-table tbody tr:nth-child(5) td:first-child').text(), "10", "initial fifth row"); 94 | 95 | var expectSecondColumnStillDescending = expect(function () { 96 | test.equal($('.reactive-table tbody tr:first-child td:nth-child(2)').text(), "15", "2nd column descending first row"); 97 | test.equal($('.reactive-table tbody tr:nth-child(2) td:nth-child(2)').text(), "10", "2nd column descending second row"); 98 | test.equal($('.reactive-table tbody tr:nth-child(5) td:nth-child(2)').text(), "5", "2nd column descending fifth row"); 99 | Blaze.remove(table); 100 | }); 101 | 102 | var expectDescending = expect(function () { 103 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "15", "descending first row"); 104 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "10", "descending second row"); 105 | test.equal($('.reactive-table tbody tr:nth-child(5) td:first-child').text(), "5", "descending fifth row"); 106 | 107 | insert1stField.set(true); 108 | Meteor.setTimeout(expectSecondColumnStillDescending, 0); 109 | }); 110 | 111 | $('.reactive-table th:first-child').click(); 112 | Meteor.setTimeout(expectDescending, 0); 113 | }]); 114 | 115 | testAsyncMulti('Sorting - server-side', [function (test, expect) { 116 | var table = Blaze.renderWithData( 117 | Template.reactiveTable, 118 | {collection: 'collection', fields: ['name', 'score']}, 119 | document.body 120 | ); 121 | 122 | var expectSecondColumn = expect(function () { 123 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "2nd column first row"); 124 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Ada Lovelace", "2nd column second row"); 125 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Marie Curie", "2nd column fourth row"); 126 | 127 | Blaze.remove(table); 128 | }); 129 | 130 | var expectAscending = expect(function () { 131 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "ascending first row"); 132 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Carl Friedrich Gauss", "ascending second row"); 133 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Grace Hopper", "ascending fourth row"); 134 | 135 | $('.reactive-table th:nth-child(2)').click(); 136 | Meteor.setTimeout(expectSecondColumn, 500); 137 | }); 138 | 139 | var expectDescending = expect(function () { 140 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "descending first row"); 141 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Marie Curie", "descending second row"); 142 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "descending fourth row"); 143 | 144 | $('.reactive-table th:first-child').click(); 145 | Meteor.setTimeout(expectAscending, 500); 146 | }); 147 | 148 | var expectDefaultAscending = expect(function () { 149 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "initial first row"); 150 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Carl Friedrich Gauss", "initial second row"); 151 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Grace Hopper", "initial fourth row"); 152 | 153 | $('.reactive-table th:first-child').click(); 154 | Meteor.setTimeout(expectDescending, 1000); 155 | }); 156 | 157 | Meteor.setTimeout(expectDefaultAscending, 1000); 158 | }]); 159 | 160 | testAsyncMulti('Sorting - virtual columns', [function (test, expect) { 161 | var table = Blaze.renderWithData( 162 | Template.reactiveTable, 163 | { 164 | collection: collection, 165 | fields: [ 166 | {key: 'name', label: 'First two letters', fn: function (name) { return name.slice(0, 2); }}, 167 | {key: 'name', label: 'Second letter', sortByValue: true, fn: function (name) { return name.slice(1, 2); }} 168 | ] 169 | }, 170 | document.body 171 | ); 172 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ad", "initial first row"); 173 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Ca", "initial second row"); 174 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Gr", "initial fourth row"); 175 | 176 | var expectSecondColumnDescending = expect(function () { 177 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ni", "2nd column descending first row"); 178 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Ma", "2nd column descending second row"); 179 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Cl", "2nd column descending fourth row"); 180 | Blaze.remove(table); 181 | }); 182 | 183 | var expectNoChange = expect(function () { 184 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ad", "2nd column first row"); 185 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Ca", "2nd column second row"); 186 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Gr", "2nd column fourth row"); 187 | 188 | $('.reactive-table th:nth-child(2)').click(); 189 | Meteor.setTimeout(expectSecondColumnDescending, 0); 190 | }); 191 | 192 | $('.reactive-table th:nth-child(2)').click(); 193 | Meteor.setTimeout(expectNoChange, 0); 194 | }]); 195 | 196 | testAsyncMulti('Sorting - multi-column', [function (test, expect) { 197 | var table = Blaze.renderWithData( 198 | Template.reactiveTable, 199 | {collection: collection}, 200 | document.body 201 | ); 202 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "initial first row"); 203 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Carl Friedrich Gauss", "initial second row"); 204 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Grace Hopper", "initial fourth row"); 205 | 206 | var expectSecondColumnAscending = expect(function () { 207 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "2nd column first row"); 208 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Marie Curie", "2nd column second row"); 209 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Ada Lovelace", "2nd column fourth row"); 210 | Blaze.remove(table); 211 | }); 212 | 213 | var expectSecondColumnDescending = expect(function () { 214 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "2nd column descending first row"); 215 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Grace Hopper", "2nd column descending second row"); 216 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "2nd column descending fourth row"); 217 | 218 | $('.reactive-table th:nth-child(2)').click(); 219 | Meteor.setTimeout(expectSecondColumnAscending, 0); 220 | }); 221 | 222 | var expectFirstColumnDescending = expect(function () { 223 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "first column descending first row"); 224 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Marie Curie", "first column descending second row"); 225 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "first column descending fourth row"); 226 | 227 | $('.reactive-table th:nth-child(2)').click(); 228 | Meteor.setTimeout(expectSecondColumnDescending, 0); 229 | }); 230 | 231 | $('.reactive-table th:first-child').click(); 232 | Meteor.setTimeout(expectFirstColumnDescending, 0); 233 | }]); 234 | 235 | testAsyncMulti('Sorting - multi-column disabled', [function (test, expect) { 236 | var table = Blaze.renderWithData( 237 | Template.reactiveTable, 238 | {collection: collection, multiColumnSort: false}, 239 | document.body 240 | ); 241 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "initial first row"); 242 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Carl Friedrich Gauss", "initial second row"); 243 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Grace Hopper", "initial fourth row"); 244 | 245 | var expectSecondColumnAscending = expect(function () { 246 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "2nd column first row"); 247 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Ada Lovelace", "2nd column second row"); 248 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "2nd column fourth row"); 249 | Blaze.remove(table); 250 | }); 251 | 252 | var expectSecondColumnDescending = expect(function () { 253 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "2nd column descending first row"); 254 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Grace Hopper", "2nd column descending second row"); 255 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Marie Curie", "2nd column descending fourth row"); 256 | 257 | $('.reactive-table th:nth-child(2)').click(); 258 | Meteor.setTimeout(expectSecondColumnAscending, 0); 259 | }); 260 | 261 | var expectFirstColumnDescending = expect(function () { 262 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "first column descending first row"); 263 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Marie Curie", "first column descending second row"); 264 | test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "first column descending fourth row"); 265 | 266 | $('.reactive-table th:nth-child(2)').click(); 267 | Meteor.setTimeout(expectSecondColumnDescending, 0); 268 | }); 269 | 270 | $('.reactive-table th:first-child').click(); 271 | Meteor.setTimeout(expectFirstColumnDescending, 0); 272 | }]); 273 | 274 | testAsyncMulti('Sorting - nested field', [function (test, expect) { 275 | var nestedObjectRows = [ 276 | {outerObject: {innerObject: 'a'}}, 277 | {outerObject: {innerObject: 'c'}}, 278 | {outerObject: {innerObject: 'b'}} 279 | ]; 280 | var table = Blaze.renderWithData( 281 | Template.reactiveTable, 282 | { 283 | collection: nestedObjectRows, 284 | fields: [{key: 'outerObject.innerObject', label: 'Column', fn: function (val) { return val; }}] 285 | }, 286 | document.body 287 | ); 288 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "a", "initial first row"); 289 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "b", "initial second row"); 290 | test.equal($('.reactive-table tbody tr:nth-child(3) td:first-child').text(), "c", "initial third row"); 291 | 292 | var expectDescending = expect(function () { 293 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "c", "descending first row"); 294 | test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "b", "descending second row"); 295 | test.equal($('.reactive-table tbody tr:nth-child(3) td:first-child').text(), "a", "descending fourth row"); 296 | 297 | Blaze.remove(table); 298 | }); 299 | 300 | $('.reactive-table th:first-child').click(); 301 | Meteor.setTimeout(expectDescending, 0); 302 | }]); -------------------------------------------------------------------------------- /test/test_settings.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('Settings - showFilter', function (test) { 2 | testTable( 3 | {collection: rows}, 4 | function () { 5 | test.length($('.reactive-table-filter'), 1, "filter should be visible by default"); 6 | } 7 | ); 8 | 9 | testTable( 10 | {collection: rows, settings: {showFilter: false}}, 11 | function () { 12 | test.length($('.reactive-table-filter'), 0, "filter should be hidden"); 13 | } 14 | ); 15 | 16 | testTable( 17 | {collection: rows, settings: {showFilter: true}}, 18 | function () { 19 | test.length($('.reactive-table-filter'), 1, "filter should be visible"); 20 | } 21 | ); 22 | 23 | testTable( 24 | {collection: rows, showFilter: false}, 25 | function () { 26 | test.length($('.reactive-table-filter'), 0, "filter should be hidden"); 27 | } 28 | ); 29 | }); 30 | 31 | Tinytest.add('Settings - rowsPerPage', function (test) { 32 | testTable( 33 | {collection: rows, settings: {rowsPerPage: 2}}, 34 | function () { 35 | test.length($('.reactive-table tbody tr'), 2, "two rows should be rendered"); 36 | } 37 | ); 38 | 39 | testTable( 40 | {collection: rows, settings: {rowsPerPage: 1}}, 41 | function () { 42 | test.length($('.reactive-table tbody tr'), 1, "one row should be rendered"); 43 | } 44 | ); 45 | 46 | testTable( 47 | {collection: rows, rowsPerPage: 2}, 48 | function () { 49 | test.length($('.reactive-table tbody tr'), 2, "two rows should be rendered"); 50 | } 51 | ); 52 | }); 53 | 54 | Tinytest.add('Settings - showNavigation', function (test) { 55 | testTable( 56 | {collection: rows}, 57 | function () { 58 | test.length($('.reactive-table-navigation'), 1, "navigation should be visible by default"); 59 | } 60 | ); 61 | 62 | testTable( 63 | {collection: rows, settings: {showNavigation: 'never'}}, 64 | function () { 65 | test.length($('.reactive-table-navigation'), 0, "navigation should be hidden"); 66 | } 67 | ); 68 | 69 | testTable( 70 | {collection: rows, settings: {showNavigation: 'always'}}, 71 | function () { 72 | test.length($('.reactive-table-navigation'), 1, "navigation should be visible"); 73 | } 74 | ); 75 | 76 | testTable( 77 | {collection: rows, settings: {showNavigation: 'auto'}}, 78 | function () { 79 | test.length($('.reactive-table-navigation'), 0, "navigation should be hidden"); 80 | } 81 | ); 82 | 83 | testTable( 84 | {collection: rows, settings: {showNavigation: 'auto', rowsPerPage: 2}}, 85 | function () { 86 | test.length($('.reactive-table-navigation'), 1, "navigation should be visible"); 87 | } 88 | ); 89 | 90 | testTable( 91 | {collection: rows, showNavigation: 'never'}, 92 | function () { 93 | test.length($('.reactive-table-navigation'), 0, "navigation should be hidden"); 94 | } 95 | ); 96 | }); 97 | 98 | 99 | Tinytest.add('Settings - showNavigationRowsPerPage', function (test) { 100 | testTable( 101 | { 102 | collection: rows, 103 | settings: { 104 | showNavigation: 'never', 105 | showNavigationRowsPerPage: true 106 | } 107 | }, 108 | function () { 109 | test.length($('.reactive-table-navigation'), 0, "navigation should be hidden"); 110 | } 111 | ); 112 | 113 | testTable( 114 | { 115 | collection: rows, 116 | showNavigation: 'always', 117 | showNavigationRowsPerPage: true 118 | }, 119 | function () { 120 | test.length($('.reactive-table-navigation .rows-per-page'), 1, "rows per page should be visible"); 121 | } 122 | ); 123 | 124 | testTable( 125 | { 126 | collection: rows, 127 | settings: { 128 | showNavigation: 'always', 129 | showNavigationRowsPerPage: false 130 | } 131 | }, 132 | function () { 133 | test.length($('.reactive-table-navigation'), 1, "navigation should be visible"); 134 | test.length($('.reactive-table-navigation .rows-per-page'), 0, "rows per page should be hidden"); 135 | } 136 | ); 137 | }); 138 | 139 | 140 | Tinytest.add('Settings - showRowCount', function (test) { 141 | testTable( 142 | { 143 | collection: rows, 144 | settings: { 145 | showNavigation: 'always', 146 | showNavigationRowsPerPage: true, 147 | } 148 | }, 149 | function () { 150 | test.length($('.reactive-table-navigation .rows-per-page .rows-per-page-count'), 0, "row count should be hidden"); 151 | } 152 | ); 153 | 154 | testTable( 155 | { 156 | collection: rows, 157 | settings: { 158 | showNavigation: 'always', 159 | showNavigationRowsPerPage: true, 160 | showRowCount: true 161 | } 162 | }, 163 | function () { 164 | test.length($('.reactive-table-navigation .rows-per-page .rows-per-page-count'), 1, "row count should be visible"); 165 | } 166 | ); 167 | }); 168 | 169 | Tinytest.add('Settings - useFontAwesome', function (test) { 170 | testTable( 171 | {collection: rows}, 172 | function () { 173 | test.length($('.reactive-table-filter .fa'), 0, "font awesome should be off by default"); 174 | } 175 | ); 176 | 177 | testTable( 178 | {collection: rows, settings: {useFontAwesome: true}}, 179 | function () { 180 | test.length($('.reactive-table-filter .fa'), 1, "font awesome should be on"); 181 | } 182 | ); 183 | 184 | testTable( 185 | {collection: rows, settings: {useFontAwesome: false}}, 186 | function () { 187 | test.length($('.reactive-table-filter .fa'), 0, "font awesome should be off"); 188 | } 189 | ); 190 | 191 | testTable( 192 | {collection: rows, useFontAwesome: true}, 193 | function () { 194 | test.length($('.reactive-table-filter .fa'), 1, "font awesome should be on"); 195 | } 196 | ); 197 | 198 | // simulate fontawesome installation 199 | Package['fortawesome:fontawesome'] = true; 200 | 201 | testTable( 202 | {collection: rows}, 203 | function () { 204 | test.length($('.reactive-table-filter .fa'), 1, "font awesome should be on"); 205 | } 206 | ); 207 | 208 | testTable( 209 | {collection: rows, useFontAwesome: false}, 210 | function () { 211 | test.length($('.reactive-table-filter .fa'), 0, "font awesome should be off"); 212 | } 213 | ); 214 | 215 | delete Package['fortawesome:fontawesome']; 216 | }); 217 | 218 | Tinytest.add('Settings - rowClass', function (test) { 219 | testTable( 220 | {collection: rows, settings: {rowClass: 'row-class'}}, 221 | function () { 222 | test.length($('.reactive-table tbody tr.row-class'), $('.reactive-table tbody tr').length, "all rows should have the class"); 223 | } 224 | ); 225 | 226 | testTable( 227 | {collection: rows, rowClass: 'row-class'}, 228 | function () { 229 | test.length($('.reactive-table tbody tr.row-class'), $('.reactive-table tbody tr').length, "all rows should have the class"); 230 | } 231 | ); 232 | 233 | testTable( 234 | { 235 | collection: rows, 236 | settings: { 237 | rowClass: function (row) { 238 | return 'row-class-' + row.score; 239 | } 240 | } 241 | }, 242 | function () { 243 | test.length($('.reactive-table tbody tr.row-class-10'), 1, "1 row should have this class"); 244 | test.length($('.reactive-table tbody tr.row-class-5'), 3, "3 rows should have this class"); 245 | } 246 | ); 247 | }); 248 | 249 | Tinytest.add('Settings - class', function (test) { 250 | testTable( 251 | {collection: rows}, 252 | function () { 253 | test.length($('.reactive-table.table.table-striped.table-hover'), 1, "table should have default classes"); 254 | } 255 | ); 256 | 257 | testTable( 258 | {collection: rows, class: 'table-class'}, 259 | function () { 260 | test.length($('.reactive-table.table-class'), 1, "table should have the class"); 261 | } 262 | ); 263 | 264 | testTable( 265 | {collection: rows, settings: {class: 'table-class'}}, 266 | function () { 267 | test.length($('.reactive-table.table-class'), 1, "table should have the class"); 268 | } 269 | ); 270 | }); 271 | 272 | Tinytest.add('Settings - id', function (test) { 273 | testTable( 274 | {collection: rows}, 275 | function () { 276 | test.equal($('.reactive-table').attr('id').slice(0, 15), 'reactive-table-', "table should have default id"); 277 | } 278 | ); 279 | 280 | testTable( 281 | {collection: rows, id: 'unique-table'}, 282 | function () { 283 | test.length($('#unique-table.reactive-table'), 1, "table should have the id"); 284 | } 285 | ); 286 | 287 | testTable( 288 | {collection: rows, settings: {id: 'unique-table-2'}}, 289 | function () { 290 | test.length($('#unique-table-2.reactive-table'), 1, "table should have the id"); 291 | } 292 | ); 293 | }); 294 | 295 | testAsyncMulti('Settings - noDataTmpl', [function (test, expect) { 296 | var collection = new Mongo.Collection(); 297 | var id; 298 | 299 | var table = Blaze.renderWithData( 300 | Template.reactiveTable, 301 | {collection: collection, noDataTmpl: Template.noData, fields: ['name', 'score']}, 302 | document.body 303 | ); 304 | 305 | var expectNoData = expect(function () { 306 | test.length($('.no-data'), 1, "no data template should be rendered"); 307 | test.length($('.reactive-table'), 0, "table should not be rendered"); 308 | 309 | Blaze.remove(table); 310 | }); 311 | 312 | var expectFilteredToNoData = expect(function () { 313 | test.length($('.no-data'), 1, "no data template should be rendered"); 314 | test.length($('.reactive-table'), 0, "table should not be rendered"); 315 | 316 | collection.remove(id); 317 | Meteor.setTimeout(expectNoData, 0); 318 | }) 319 | 320 | var expectTable = expect(function () { 321 | test.length($('.no-data'), 0, "no data template should not be rendered"); 322 | test.length($('.reactive-table tbody tr'), 1, "second page should have one rows"); 323 | 324 | $('.reactive-table-filter input').val('g'); 325 | $('.reactive-table-filter input').trigger('input'); 326 | Meteor.setTimeout(expectFilteredToNoData, 1000); 327 | }); 328 | 329 | test.length($('.no-data'), 1, "no data template should be rendered"); 330 | test.length($('.reactive-table'), 0, "table should not be rendered"); 331 | 332 | id = collection.insert({name: 'Ada Lovelace', score: 5}); 333 | Meteor.setTimeout(expectTable, 0); 334 | }]); 335 | 336 | testAsyncMulti('Settings - currentPage var updates table', [function (test, expect) { 337 | var page = new ReactiveVar(0); 338 | 339 | var table = Blaze.renderWithData( 340 | Template.reactiveTable, 341 | {collection: rows, settings: {rowsPerPage: 2, currentPage: page}}, 342 | document.body 343 | ); 344 | 345 | var expectFirstPage = expect(function () { 346 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "should be on first page"); 347 | test.length($('.reactive-table tbody tr'), 2, "first page should have two rows"); 348 | test.equal($('.reactive-table-navigation .page-number input').val(), "1", "displayed page number should be 1"); 349 | 350 | Blaze.remove(table); 351 | }); 352 | 353 | var expectSecondPage = expect(function () { 354 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Claude Shannon", "should be on the second page"); 355 | test.length($('.reactive-table tbody tr'), 2, "second page should have two rows"); 356 | test.equal($('.reactive-table-navigation .page-number input').val(), "2", "displayed page number should be 2"); 357 | 358 | page.set(0); 359 | Meteor.setTimeout(expectFirstPage, 0); 360 | }); 361 | 362 | var expectLastPage = expect(function () { 363 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Marie Curie", "should be on last page"); 364 | test.length($('.reactive-table tbody tr'), 2, "last page should have two rows"); 365 | test.equal($('.reactive-table-navigation .page-number input').val(), "3", "displayed page number should be 3"); 366 | 367 | page.set(1); 368 | Meteor.setTimeout(expectSecondPage, 0); 369 | }); 370 | 371 | page.set(2); 372 | Meteor.setTimeout(expectLastPage, 0); 373 | }]); 374 | 375 | testAsyncMulti('Settings - currentPage var updates table with server-side collection', [function (test, expect) { 376 | var page = new ReactiveVar(0); 377 | 378 | var table = Blaze.renderWithData( 379 | Template.reactiveTable, 380 | {collection: "collection", fields: ["name"], settings: {rowsPerPage: 2, currentPage: page}}, 381 | document.body 382 | ); 383 | 384 | var expectFirstPage = expect(function () { 385 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "should be on first page"); 386 | test.length($('.reactive-table tbody tr'), 2, "first page should have two rows"); 387 | test.equal($('.reactive-table-navigation .page-number input').val(), "1", "displayed page number should be 1"); 388 | 389 | Blaze.remove(table); 390 | }); 391 | 392 | var expectSecondPage = expect(function () { 393 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Claude Shannon", "should be on the second page"); 394 | test.length($('.reactive-table tbody tr'), 2, "second page should have two rows"); 395 | test.equal($('.reactive-table-navigation .page-number input').val(), "2", "displayed page number should be 2"); 396 | 397 | page.set(0); 398 | Meteor.setTimeout(expectFirstPage, 500); 399 | }); 400 | 401 | var expectLastPage = expect(function () { 402 | test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Marie Curie", "should be on last page"); 403 | test.length($('.reactive-table tbody tr'), 2, "last page should have two rows"); 404 | test.equal($('.reactive-table-navigation .page-number input').val(), "3", "displayed page number should be 3"); 405 | 406 | page.set(1); 407 | Meteor.setTimeout(expectSecondPage, 500); 408 | }); 409 | 410 | page.set(2); 411 | Meteor.setTimeout(expectLastPage, 500); 412 | }]); 413 | 414 | testAsyncMulti('Settings - currentPage var updated from table changes', [function (test, expect) { 415 | var page = new ReactiveVar(0); 416 | 417 | var table = Blaze.renderWithData( 418 | Template.reactiveTable, 419 | {collection: rows, settings: {rowsPerPage: 2, currentPage: page}}, 420 | document.body 421 | ); 422 | 423 | var expectFirstPage = expect(function () { 424 | test.equal(page.get(), 0, "should be on first page"); 425 | 426 | Blaze.remove(table); 427 | }); 428 | 429 | var expectSecondPage = expect(function () { 430 | test.equal(page.get(), 1, "should be on second page"); 431 | 432 | $('.reactive-table-navigation .page-number input').val("1"); 433 | $('.reactive-table-navigation .page-number input').trigger("change"); 434 | Meteor.setTimeout(expectFirstPage, 0); 435 | }); 436 | 437 | var expectLastPage = expect(function () { 438 | test.equal(page.get(), 2, "should be on last page"); 439 | 440 | $('.reactive-table-navigation .page-number input').val('2'); 441 | $('.reactive-table-navigation .page-number input').trigger("change"); 442 | Meteor.setTimeout(expectSecondPage, 0); 443 | }); 444 | 445 | $('.reactive-table-navigation .page-number input').val("3"); 446 | $('.reactive-table-navigation .page-number input').trigger("change"); 447 | Meteor.setTimeout(expectLastPage, 0); 448 | }]); 449 | 450 | testAsyncMulti('Settings - rowsPerPage var updates table', [function (test, expect) { 451 | var rowsPerPage = new ReactiveVar(5); 452 | 453 | var table = Blaze.renderWithData( 454 | Template.reactiveTable, 455 | {collection: rows, settings: {rowsPerPage: rowsPerPage}}, 456 | document.body 457 | ); 458 | 459 | var expectThreeRows = expect(function () { 460 | test.length($('.reactive-table tbody tr'), 3, "three rows should be rendered"); 461 | 462 | Blaze.remove(table); 463 | }); 464 | 465 | var expectFiveRows = expect(function () { 466 | test.length($('.reactive-table tbody tr'), 5, "five rows should be rendered"); 467 | 468 | rowsPerPage.set(3); 469 | Meteor.setTimeout(expectThreeRows, 0); 470 | }); 471 | 472 | Meteor.setTimeout(expectFiveRows, 0); 473 | }]); 474 | 475 | testAsyncMulti('Settings - rowsPerPage var updates table with server-side collection', [function (test, expect) { 476 | var rowsPerPage = new ReactiveVar(5); 477 | 478 | var table = Blaze.renderWithData( 479 | Template.reactiveTable, 480 | {collection: "collection", fields: ["name"], settings: {rowsPerPage: rowsPerPage}}, 481 | document.body 482 | ); 483 | 484 | var expectThreeRows = expect(function () { 485 | test.length($('.reactive-table tbody tr'), 3, "three rows should be rendered"); 486 | 487 | Blaze.remove(table); 488 | }); 489 | 490 | var expectFiveRows = expect(function () { 491 | test.length($('.reactive-table tbody tr'), 5, "five rows should be rendered"); 492 | 493 | rowsPerPage.set(3); 494 | Meteor.setTimeout(expectThreeRows, 500); 495 | }); 496 | 497 | Meteor.setTimeout(expectFiveRows, 500); 498 | }]); 499 | 500 | testAsyncMulti('Settings - rowsPerPage var updated from table changes', [function (test, expect) { 501 | var rowsPerPage = new ReactiveVar(5); 502 | 503 | var table = Blaze.renderWithData( 504 | Template.reactiveTable, 505 | {collection: rows, settings: {rowsPerPage: rowsPerPage}}, 506 | document.body 507 | ); 508 | 509 | var expectThreeRows = expect(function () { 510 | test.equal(rowsPerPage.get(), 3, "three rows should be rendered"); 511 | 512 | Blaze.remove(table); 513 | }); 514 | 515 | $('.reactive-table-navigation .rows-per-page input').val(3); 516 | $('.reactive-table-navigation .rows-per-page input').trigger('change'); 517 | Meteor.setTimeout(expectThreeRows, 0); 518 | }]); 519 | --------------------------------------------------------------------------------