diff --git a/.npm/package/.gitignore b/.npm/package/.gitignore deleted file mode 100644 index 3c3629e..0000000 --- a/.npm/package/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/.npm/package/README b/.npm/package/README deleted file mode 100644 index 3d49255..0000000 --- a/.npm/package/README +++ /dev/null @@ -1,7 +0,0 @@ -This directory and the files immediately inside it are automatically generated -when you change this package's NPM dependencies. Commit the files in this -directory (npm-shrinkwrap.json, .gitignore, and this README) to source control -so that others run the same versions of sub-dependencies. - -You should NOT check in the node_modules directory that Meteor automatically -creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/.npm/package/npm-shrinkwrap.json b/.npm/package/npm-shrinkwrap.json deleted file mode 100644 index 84fe8dc..0000000 --- a/.npm/package/npm-shrinkwrap.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "lockfileVersion": 1, - "dependencies": { - "datatables.net": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.0.8.tgz", - "integrity": "sha512-4/2dYx4vl975zQqZbyoVEm0huPe61qffjBRby7K7V+y9E+ORq4R8KavkgrNMmIgO6cl85Pg4AvCbVjvPCIT1Yg==" - }, - "jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" - } - } -} diff --git a/.versions b/.versions index 6b0a62b..61b8746 100644 --- a/.versions +++ b/.versions @@ -1,65 +1,69 @@ -aldeed:tabular@3.0.0-rc.0 -allow-deny@1.1.1 +aldeed:tabular@3.0.0-rc.4 +allow-deny@2.0.0 anti:fake@0.4.1 -babel-compiler@7.10.5 -babel-runtime@1.5.1 -base64@1.0.12 -binary-heap@1.0.11 -blaze@2.9.0 -blaze-tools@1.0.10 -boilerplate-generator@1.7.2 -caching-compiler@1.2.2 -caching-html-compiler@1.0.5 -callback-hook@1.5.1 -check@1.4.1 -ddp@1.4.1 -ddp-client@2.6.2 -ddp-common@1.4.1 -ddp-server@2.7.1 -diff-sequence@1.1.2 -dynamic-import@0.7.3 -ecmascript@0.16.8 -ecmascript-runtime@0.8.1 -ecmascript-runtime-client@0.12.1 -ecmascript-runtime-server@0.11.0 -ejson@1.1.3 -fetch@0.1.4 -geojson-utils@1.0.11 -html-tools@1.0.11 -htmljs@1.2.1 -id-map@1.1.1 -inter-process-messaging@0.1.1 -local-test:aldeed:tabular@3.0.0-rc.0 -logging@1.3.4 -meteor@1.11.5 -minimongo@1.9.4 -modern-browsers@0.1.10 -modules@0.20.0 -modules-runtime@0.13.1 -mongo@1.16.10 -mongo-decimal@0.1.3 -mongo-dev-server@1.1.0 -mongo-id@1.0.8 -npm-mongo@4.17.2 -observe-sequence@1.0.21 -ordered-dict@1.1.0 -promise@0.12.2 -random@1.2.1 -react-fast-refresh@0.2.8 -reactive-dict@1.3.1 -reactive-var@1.0.12 -reload@1.3.1 -retry@1.1.0 -routepolicy@1.1.1 -session@1.2.1 -socket-stream-client@0.5.2 -spacebars@1.0.10 -spacebars-compiler@1.1.0 -templating@1.1.8 -templating-tools@1.1.1 -tinytest@1.2.3 -tracker@1.3.3 -typescript@4.9.5 -underscore@1.6.1 -webapp@1.13.8 -webapp-hashing@1.1.1 +babel-compiler@7.11.0 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +blaze@3.0.0 +blaze-tools@2.0.0 +boilerplate-generator@2.0.0 +caching-compiler@2.0.0 +caching-html-compiler@2.0.0 +callback-hook@1.6.0 +check@1.4.2 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.0.1 +ddp-common@1.4.4 +ddp-server@3.0.1 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.9 +ecmascript-runtime@0.8.2 +ecmascript-runtime-client@0.12.2 +ecmascript-runtime-server@0.11.1 +ejson@1.1.4 +facts-base@1.0.2 +fetch@0.1.5 +geojson-utils@1.0.12 +html-tools@2.0.0 +htmljs@2.0.1 +id-map@1.2.0 +inter-process-messaging@0.1.2 +local-test:aldeed:tabular@3.0.0-rc.4 +logging@1.3.5 +meteor@2.0.1 +minimongo@2.0.1 +modern-browsers@0.1.11 +modules@0.20.1 +modules-runtime@0.13.2 +mongo@2.0.2 +mongo-decimal@0.1.4-beta300.7 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@4.17.4 +observe-sequence@2.0.0 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +reactive-dict@1.3.2 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +session@1.2.2 +socket-stream-client@0.5.3 +spacebars@2.0.0 +spacebars-compiler@2.0.0 +templating@1.4.4 +templating-compiler@2.0.0 +templating-runtime@2.0.0 +templating-tools@2.0.0 +tinytest@1.3.0 +tracker@1.3.4 +typescript@5.4.3 +underscore@1.6.4 +webapp@2.0.1 +webapp-hashing@1.1.2 diff --git a/README.md b/README.md index 8bc33a9..4abd49e 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -aldeed:tabular -========================= +# aldeed:tabular A Meteor package that creates reactive [DataTables](http://datatables.net/) in an efficient way, allowing you to display the contents of enormous collections without impacting app performance. @@ -50,14 +49,12 @@ Please open an issue if you like to help out with maintenance on this package. -## ATTENTION: Updating to 2.0 +## ATTENTION: Release v3 and datatables 2.x -Version 2.0 API is backwards compatible other than the following changes: -- Requires Meteor 1.3+ -- You must explicitly import the `Tabular` object into every file where you use it. (`import Tabular from 'meteor/aldeed:tabular';`) -- You must configure the Bootstrap theme (or whatever theme you want) yourself. See [Installing and Configuring a Theme](#installing-and-configuring-a-theme) +Version 3.0 is Meteor 3 compatible but **slightly breaking** in order to support more flexibility in importing +different versions of DataTables. -This version also includes a few fixes and a few new features. +Read the respective sections on how to install/setup aldeed:tabular v3 ## Features @@ -87,27 +84,58 @@ This example is for the Bootstrap theme. You can use another theme package. See First: ```bash -$ npm install --save jquery@1.12.1 datatables.net-bs +$ meteor add jquery@3.0.2 +$ npm install --save jquery@latest datatables.net-bs@latest ``` -Note that we install jquery@1.12.1. This needs to match the current version of jQuery included with Meteor's `jquery` package. (See the version comment in https://github.com/meteor/meteor/blob/master/packages/non-core/jquery/package.js) Otherwise, due to the `datatables.net` package depending on `jquery` NPM package, it might automatically install the latest `jquery` version, which may conflict with Bootstrap or Meteor. +> Heads up! If you want to use datatables 2.x and newer, you need to [recompile these modules](https://guide.meteor.com/using-npm-packages#recompile) +> or they will not detect the correct jquery module :-( + +FOr this, open `package.json` and add to the `"meteor": {...}` the the nodes modules to recompile: + +```json +{ + "meteor": { + "nodeModules": { + "recompile": { + "datatables.net": ["client", "legacy"], + "datatables.net-bs": ["client", "legacy"] + } + } + } +} +``` + +In this example the datatables core and bootstrap are recopmiled. +While core (`datatables.net`) is always to be added, the other packages may vary, depending on your needs. Then, somewhere in your client JavaScript: +**Datatables 1.x** + ```js import { $ } from 'meteor/jquery'; +import Tabular from 'meteor/aldeeed:tabular' import dataTablesBootstrap from 'datatables.net-bs'; import 'datatables.net-bs/css/dataTables.bootstrap.css'; +// and maybe other DT imports + dataTablesBootstrap(window, $); +// and maybe other DT-init calls + +// finally initializing Tabular +Tabular.init() ``` -## Online Demo App +**Datatables 2.x** -View a [demonstration project on Meteorpad](http://meteorpad.com/pad/xNafF9N5XJNrFJEyG/TabularDemo). +```js +import DataTables from 'datatables.net-bs'; +import 'datatables.net-bs/css/dataTables.bootstrap.css'; +// and maybe other DT imports -Another example app courtesy of @AnnotatedJS: -* Hosted app: http://greatalbums.meteor.com/albums (You can sign in with email "admin@demo.com" and password "password") -* Source: https://github.com/AnnotatedJS/GreatAlbums +Tabular.init({ DataTables }) +``` ## Example @@ -604,6 +632,10 @@ For package names for other themes, see https://datatables.net/download/npm Once the packages are installed, you need to import them in one of your client JavaScript files: +
+datatables.net 1.x + + ```js import { $ } from 'meteor/jquery'; @@ -629,6 +661,47 @@ flashExportButtons(window, $); printButton(window, $); ``` +
+ + +
+datatables.net >= 2.x + +You need to have Meteor [to recompile the packages](https://guide.meteor.com/using-npm-packages#recompile): + +```json +{ + ... + "meteor": { + ... + "nodeModules": { + "recompile": { + "datatables.net": ["client", "legacy"], + "datatables.net-bs": ["client", "legacy"], + "datatables.net-buttons": ["client", "legacy"], + "datatables.net-buttons-bs": ["client", "legacy"] + } + } + } +} +``` + +```js +// Bootstrap Theme +import 'datatables.net-bs'; +import 'datatables.net-bs/css/dataTables.bootstrap.css'; + +// Buttons Core +import 'datatables.net-buttons-bs'; +// Import whichever buttons you are using +import 'datatables.net-buttons/js/buttons.colVis.js'; +import 'datatables.net-buttons/js/buttons.html5.js'; +import 'datatables.net-buttons/js/buttons.flash.js'; +import 'datatables.net-buttons/js/buttons.print.js'; +``` + +
+ Finally, for the Tabular tables that need them, add the `buttons` and `buttonContainer` options. The `buttons` option is part of DataTables and is documented here: https://datatables.net/extensions/buttons/ The `buttonContainer` option is part of `aldeed:tabular` and does the tricky task of appending the buttons to some element in the generated table. Set it to the CSS selector for the container. Bootstrap example: diff --git a/client/main.js b/client/main.js index 3a0d41b..4fc3bf5 100644 --- a/client/main.js +++ b/client/main.js @@ -1,558 +1,572 @@ +/* global _, Blaze, Tracker, ReactiveVar, Session, Meteor, $ */ import './tabular.html'; - -/* global _, Blaze, Tracker, ReactiveVar, Session, Meteor, */ -import { $ } from 'meteor/jquery'; -//This is a bit shit that we're initialising this explicit version within the library -import 'datatables.net-bs5'; - import { Mongo } from 'meteor/mongo'; import { Template } from 'meteor/templating'; - import Tabular from '../common/Tabular'; import tableInit from './tableInit'; import getPubSelector from './getPubSelector'; import { getMongoSort, objectsAreEqual, sortsAreEqual } from '../common/util'; -//dataTableInit(window, $); Template.registerHelper('TabularTables', Tabular.tablesByName); Tabular.tableRecords = new Mongo.Collection('tabular_records'); Tabular.remoteTableRecords = []; -Tabular.getTableRecordsCollection = function (connection) { - if (!connection || connection === Tabular.tableRecords._connection) { - return Tabular.tableRecords; - } +/** + * Initialize tabular after you've imported your DataTable code, so + * there are no conflicts or race conditions. + * + * Dependening on which datatables versions you use, + * you might want to pass the DataTable constructor directly. + * + * @param DataTable {object=} only pass this instance if you're using DataTables >= 2.x + */ +Tabular.init = ({ DataTable } = {}) => { + Tabular.getTableRecordsCollection = function (connection) { + if (!connection || connection === Tabular.tableRecords._connection) { + return Tabular.tableRecords; + } - let remote = _.find(Tabular.remoteTableRecords, (remote) => remote.connection === connection); - if (!remote) { - remote = { - connection, - tableRecords: new Mongo.Collection('tabular_records', { connection }) - }; - Tabular.remoteTableRecords.push(remote); - } - return remote.tableRecords; -}; - -Tabular.getRecord = function (name, collection) { - return Tabular.getTableRecordsCollection(collection._connection).findOne(name); -}; - -Template.tabular.helpers({ - atts() { - // We remove the "table" and "selector" attributes and assume the rest belong - // on the element - return _.omit(this, 'table', 'selector'); + let remote = _.find(Tabular.remoteTableRecords, (remote) => remote.connection === connection); + if (!remote) { + remote = { + connection, + tableRecords: new Mongo.Collection('tabular_records', { connection }) + }; + Tabular.remoteTableRecords.push(remote); + } + return remote.tableRecords; + }; + + Tabular.getRecord = function (name, collection) { + return Tabular.getTableRecordsCollection(collection._connection).findOne(name); + }; + + Template.tabular.helpers({ + atts () { + // We remove the "table" and "selector" attributes and assume the rest belong + // on the
element + return _.omit(this, 'table', 'selector'); + } + }); + + + const createDatatable = (template, options) => { + return DataTable + ? new DataTable(template.data?.id + ? `#${template.data?.id}` + :template.$tableElement.get(0), options) + : template.$tableElement.DataTable(options) } -}); - -Template.tabular.onRendered(function () { - const template = this; - template.$tableElement = template.$('table'); - let table; - let resetTablePaging = false; - - template.tabular = {}; - template.tabular.data = []; - template.tabular.pubSelector = new ReactiveVar({}, objectsAreEqual); - template.tabular.skip = new ReactiveVar(0); - template.tabular.limit = new ReactiveVar(10); - template.tabular.sort = new ReactiveVar(null, sortsAreEqual); - template.tabular.columns = null; - template.tabular.fields = null; - template.tabular.searchFields = null; - template.tabular.searchCaseInsensitive = true; - template.tabular.splitSearchByWhitespace = true; - template.tabular.tableName = new ReactiveVar(null); - template.tabular.options = new ReactiveVar({}, objectsAreEqual); - template.tabular.docPub = new ReactiveVar(null); - template.tabular.collection = new ReactiveVar(null); - template.tabular.connection = null; - template.tabular.ready = new ReactiveVar(false); - template.tabular.recordsTotal = 0; - template.tabular.recordsFiltered = 0; - template.tabular.isLoading = new ReactiveVar(true); - template.tabular.blazeViews = []; - template.tabular.searchTerm = new ReactiveVar(this.data.searchTerm || null); - - // These are some DataTables options that we need for everything to work. - // We add them to the options specified by the user. - const ajaxOptions = { - // tell DataTables that we're getting the table data from a server - serverSide: true, - processing: true, - // define the function that DataTables will call upon first load and whenever - // we tell it to reload data, such as when paging, etc. - ajax: function (data, callback /*, settings*/) { - // When DataTables requests data, first we set - // the new skip, limit, order, and pubSelector values - // that DataTables has requested. These trigger - // the first subscription, which will then trigger the - // second subscription. - - //console.log( 'data', data, 'template.tabular.data', template.tabular.data ); - - // Update skip - template.tabular.skip.set(data.start); - Session.set('Tabular.LastSkip', data.start); - - // Update limit - let options = template.tabular.options.get(); - let hardLimit = options && options.limit; - if (data.length === -1) { - if (hardLimit === undefined) { - console.warn( - 'When using no paging or an "All" option with tabular, it is best to also add a hard limit in your table options like {limit: 500}' - ); - template.tabular.limit.set(null); - } else { - template.tabular.limit.set(hardLimit); + + Template.tabular.onRendered(function () { + const template = this; + template.$tableElement = template.$('table'); + let table; + let resetTablePaging = false; + + template.tabular = {}; + template.tabular.data = []; + template.tabular.pubSelector = new ReactiveVar({}, objectsAreEqual); + template.tabular.skip = new ReactiveVar(0); + template.tabular.limit = new ReactiveVar(10); + template.tabular.sort = new ReactiveVar(null, sortsAreEqual); + template.tabular.columns = null; + template.tabular.fields = null; + template.tabular.searchFields = null; + template.tabular.searchCaseInsensitive = true; + template.tabular.splitSearchByWhitespace = true; + template.tabular.tableName = new ReactiveVar(null); + template.tabular.options = new ReactiveVar({}, objectsAreEqual); + template.tabular.docPub = new ReactiveVar(null); + template.tabular.collection = new ReactiveVar(null); + template.tabular.connection = null; + template.tabular.ready = new ReactiveVar(false); + template.tabular.recordsTotal = 0; + template.tabular.recordsFiltered = 0; + template.tabular.isLoading = new ReactiveVar(true); + template.tabular.blazeViews = []; + template.tabular.searchTerm = new ReactiveVar(this.data.searchTerm || null); + + // These are some DataTables options that we need for everything to work. + // We add them to the options specified by the user. + const ajaxOptions = { + // tell DataTables that we're getting the table data from a server + serverSide: true, + processing: true, + // define the function that DataTables will call upon first load and whenever + // we tell it to reload data, such as when paging, etc. + ajax: function (data, callback /*, settings*/) { + // When DataTables requests data, first we set + // the new skip, limit, order, and pubSelector values + // that DataTables has requested. These trigger + // the first subscription, which will then trigger the + // second subscription. + + //console.log( 'data', data, 'template.tabular.data', template.tabular.data ); + + // Update skip + template.tabular.skip.set(data.start); + Session.set('Tabular.LastSkip', data.start); + + // Update limit + let options = template.tabular.options.get(); + let hardLimit = options && options.limit; + if (data.length === -1) { + if (hardLimit === undefined) { + console.warn( + 'When using no paging or an "All" option with tabular, it is best to also add a hard limit in your table options like {limit: 500}' + ); + template.tabular.limit.set(null); + } + else { + template.tabular.limit.set(hardLimit); + } + } + else { + template.tabular.limit.set(data.length); } - } else { - template.tabular.limit.set(data.length); - } - // Update sort - template.tabular.sort.set(getMongoSort(data.order, options.columns)); - - // Update pubSelector - let pubSelector = template.tabular.selector; - //if we're using the searchCustom functionality don't do the default client side regex via getPubSelector - if (!template.tabular.tableDef.searchCustom) { - pubSelector = getPubSelector( - template.tabular.selector, - (data.search && data.search.value) || null, - template.tabular.searchFields, - template.tabular.searchCaseInsensitive, - template.tabular.splitSearchByWhitespace, - data.columns || null, - options.columns - ); - } - template.tabular.pubSelector.set(pubSelector); + // Update sort + template.tabular.sort.set(getMongoSort(data.order, options.columns)); + + // Update pubSelector + let pubSelector = template.tabular.selector; + //if we're using the searchCustom functionality don't do the default client side regex via getPubSelector + if (!template.tabular.tableDef.searchCustom) { + pubSelector = getPubSelector( + template.tabular.selector, + (data.search && data.search.value) || null, + template.tabular.searchFields, + template.tabular.searchCaseInsensitive, + template.tabular.splitSearchByWhitespace, + data.columns || null, + options.columns + ); + } + template.tabular.pubSelector.set(pubSelector); - // We're ready to subscribe to the data. - // Matters on the first run only. - template.tabular.ready.set(true); + // We're ready to subscribe to the data. + // Matters on the first run only. + template.tabular.ready.set(true); - //console.log('ajax'); - //console.debug( 'calling ajax callback with', template.tabular.data ); + //console.log('ajax'); + //console.debug( 'calling ajax callback with', template.tabular.data ); - callback({ - draw: data.draw, - recordsTotal: template.tabular.recordsTotal, - recordsFiltered: template.tabular.recordsFiltered, - data: template.tabular.data - }); - }, - initComplete: function () { - // Fix THOMAS modified 24.11.2021 - // Fix the case of multiple table on the same page - const tableId = template.data.id; - const options = template.tabular.options.get(); - if (options.search && options.search.onEnterOnly) { - const replaceSearchLabel = function (newText) { - $('#' + tableId + '_filter label') - .contents() - .filter(function () { - return this.nodeType === 3 && this.textContent.trim().length; - }) - .replaceWith(newText); - }; - $('#' + tableId + '_filter input') - .unbind() - .bind('keyup change', function (event) { - if (!table) return; - if (event.keyCode === 13 || this.value === '') { - replaceSearchLabel(table.i18n('search')); - table.search(this.value).draw(); - } else { - replaceSearchLabel(table.i18n('Press enter to filter')); + callback({ + draw: data.draw, + recordsTotal: template.tabular.recordsTotal, + recordsFiltered: template.tabular.recordsFiltered, + data: template.tabular.data + }); + }, + initComplete: function () { + // Fix THOMAS modified 24.11.2021 + // Fix the case of multiple table on the same page + const tableId = template.data.id; + const options = template.tabular.options.get(); + if (options.search && options.search.onEnterOnly) { + const replaceSearchLabel = function (newText) { + $('#' + tableId + '_filter label') + .contents() + .filter(function () { + return this.nodeType === 3 && this.textContent.trim().length; + }) + .replaceWith(newText); + }; + $('#' + tableId + '_filter input') + .unbind() + .bind('keyup change', function (event) { + if (!table) return; + if (event.keyCode === 13 || this.value === '') { + replaceSearchLabel(table.i18n('search')); + table.search(this.value).draw(); + } + else { + replaceSearchLabel(table.i18n('Press enter to filter')); + } + }); + } + }, + headerCallback (headerRow) { + const options = template.tabular.options.get(); + const columns = options.columns; + + $(headerRow) + .find('td,th') + .each((index, headerCell) => { + const titleFunction = columns[index] && columns[index].titleFn; + if (typeof titleFunction === 'function') { + headerCell.innerHTML = ''; + if (headerCell.__blazeViewInstance) { + Blaze.remove(headerCell.__blazeViewInstance); + } + const view = new Blaze.View(titleFunction); + headerCell.__blazeViewInstance = Blaze.render(view, headerCell); } }); } - }, - headerCallback(headerRow) { - const options = template.tabular.options.get(); - const columns = options.columns; - - $(headerRow) - .find('td,th') - .each((index, headerCell) => { - const titleFunction = columns[index] && columns[index].titleFn; - if (typeof titleFunction === 'function') { - headerCell.innerHTML = ''; - if (headerCell.__blazeViewInstance) { - Blaze.remove(headerCell.__blazeViewInstance); - } - const view = new Blaze.View(titleFunction); - headerCell.__blazeViewInstance = Blaze.render(view, headerCell); - } - }); - } - }; + }; - // For testing - //setUpTestingAutoRunLogging(template); + // For testing + //setUpTestingAutoRunLogging(template); - // Reactively determine table columns, fields, and searchFields. - // This will rerun whenever the current template data changes. - let lastTableName; - template.autorun(function () { - let data = Template.currentData(); + // Reactively determine table columns, fields, and searchFields. + // This will rerun whenever the current template data changes. + let lastTableName; + template.autorun(function () { + let data = Template.currentData(); - //console.log('currentData autorun', data); + // if we don't have data OR the selector didn't actually change return out + if (!data || (data.selector && template.tabular.selector === data.selector)) { + return; + } - // if we don't have data OR the selector didn't actually change return out - if (!data || (data.selector && template.tabular.selector === data.selector)) { - return; - } + // We get the current TabularTable instance, and cache it on the + // template instance for access elsewhere + let tabularTable = (template.tabular.tableDef = data.table); - // We get the current TabularTable instance, and cache it on the - // template instance for access elsewhere - let tabularTable = (template.tabular.tableDef = data.table); + if (!(tabularTable instanceof Tabular.Table)) { + throw new Error('You must pass Tabular.Table instance as the table attribute'); + } - if (!(tabularTable instanceof Tabular.Table)) { - throw new Error('You must pass Tabular.Table instance as the table attribute'); - } + // Always update the selector reactively + template.tabular.selector = data.selector; + template.tabular.searchTerm.set(data.searchTerm || null); + + // The remaining stuff relates to changing the `table` + // attribute. If we didn't change it, we can stop here, + // but we need to reload the table if this is not the first + // run + if (tabularTable.name === lastTableName) { + if (table) { + // passing `false` as the second arg tells it to + // reset the paging + table.ajax.reload(null, true); + } + return; + } + + // If we reactively changed the `table` attribute, run + // onUnload for the previous table + if (lastTableName !== undefined) { + let lastTableDef = Tabular.tablesByName[lastTableName]; + if (lastTableDef && typeof lastTableDef.onUnload === 'function') { + lastTableDef.onUnload(); + } + } - // Always update the selector reactively - template.tabular.selector = data.selector; - template.tabular.searchTerm.set(data.searchTerm || null); + // Cache this table name as the last table name for next run + lastTableName = tabularTable.name; - // The remaining stuff relates to changing the `table` - // attribute. If we didn't change it, we can stop here, - // but we need to reload the table if this is not the first - // run - if (tabularTable.name === lastTableName) { + // Figure out and update the columns, fields, and searchFields + const columns = tableInit(tabularTable, template); + + // Set/update everything else + template.tabular.searchCaseInsensitive = true; + template.tabular.splitSearchByWhitespace = true; + + if (tabularTable.options && tabularTable.options.search) { + if (tabularTable.options.search.caseInsensitive === false) { + template.tabular.searchCaseInsensitive = false; + } + if (tabularTable.options.search.smart === false) { + template.tabular.splitSearchByWhitespace = false; + } + } + template.tabular.options.set({ + ...tabularTable.options, + columns + }); + template.tabular.tableName.set(tabularTable.name); + template.tabular.docPub.set(tabularTable.pub); + template.tabular.collection.set(tabularTable.collection); + if (tabularTable.collection && tabularTable.collection._connection) { + template.tabular.connection = tabularTable.collection._connection; + } + + // userOptions rerun should do this? if (table) { - // passing `false` as the second arg tells it to + // passing `true` as the second arg tells it to // reset the paging table.ajax.reload(null, true); } - return; - } + }); + + template.autorun(() => { + // these 5 are the parameters passed to "tabular_getInfo" subscription + // so when they *change*, set the isLoading flag to true + template.tabular.tableName.get(); + template.tabular.pubSelector.get(); + template.tabular.sort.get(); + template.tabular.skip.get(); + template.tabular.limit.get(); + template.tabular.isLoading.set(true); + template.tabular.searchTerm.get(); + }); - // If we reactively changed the `table` attribute, run - // onUnload for the previous table - if (lastTableName !== undefined) { - let lastTableDef = Tabular.tablesByName[lastTableName]; - if (lastTableDef && typeof lastTableDef.onUnload === 'function') { - lastTableDef.onUnload(); + // First Subscription + // Subscribe to an array of _ids that should be on the + // current page of the table, plus some aggregate + // numbers that DataTables needs in order to show the paging. + // The server will reactively keep this info accurate. + // It's not necessary to call stop + // on subscriptions that are within autorun computations. + template.autorun(function () { + if (!template.tabular.ready.get()) { + return; } - } - // Cache this table name as the last table name for next run - lastTableName = tabularTable.name; + //console.log('tabular_getInfo autorun'); - // Figure out and update the columns, fields, and searchFields - const columns = tableInit(tabularTable, template); + function onReady () { + template.tabular.isLoading.set(false); + } - // Set/update everything else - template.tabular.searchCaseInsensitive = true; - template.tabular.splitSearchByWhitespace = true; + let connection = template.tabular.connection; + let context = connection || Meteor; + context.subscribe( + 'tabular_getInfo', + template.tabular.tableName.get(), + template.tabular.pubSelector.get(), + template.tabular.sort.get(), + template.tabular.skip.get(), + template.tabular.limit.get(), + template.tabular.searchTerm.get(), + onReady + ); + }); - if (tabularTable.options && tabularTable.options.search) { - if (tabularTable.options.search.caseInsensitive === false) { - template.tabular.searchCaseInsensitive = false; + // Second Subscription + // Reactively subscribe to the documents with _ids given to us. Limit the + // fields to only those we need to display. It's not necessary to call stop + // on subscriptions that are within autorun computations. + template.autorun(function () { + // tableInfo is reactive and causes a rerun whenever the + // list of docs that should currently be in the table changes. + // It does not cause reruns based on the documents themselves + // changing. + let tableName = template.tabular.tableName.get(); + let collection = template.tabular.collection.get(); + let tableInfo = Tabular.getRecord(tableName, collection) || {}; + + //console.log('tableName and tableInfo autorun', tableName, tableInfo); + + template.tabular.recordsTotal = tableInfo.recordsTotal || 0; + template.tabular.recordsFiltered = tableInfo.recordsFiltered || 0; + + // In some cases, there is no point in subscribing to nothing + if ( + _.isEmpty(tableInfo) || + template.tabular.recordsTotal === 0 || + template.tabular.recordsFiltered === 0 + ) { + return; } - if (tabularTable.options.search.smart === false) { - template.tabular.splitSearchByWhitespace = false; + + // Extend with extraFields from table definition + let fields = template.tabular.fields; + if (fields) { + // Extend with extraFields from table definition + if (typeof template.tabular.tableDef.extraFields === 'object') { + fields = _.extend(_.clone(fields), template.tabular.tableDef.extraFields); + } } - } - template.tabular.options.set({ - ...tabularTable.options, - columns + + template.tabular.tableDef.sub.subscribe( + template.tabular.docPub.get(), + tableName, + tableInfo.ids || [], + fields + ); }); - template.tabular.tableName.set(tabularTable.name); - template.tabular.docPub.set(tabularTable.pub); - template.tabular.collection.set(tabularTable.collection); - if (tabularTable.collection && tabularTable.collection._connection) { - template.tabular.connection = tabularTable.collection._connection; - } - // userOptions rerun should do this? - if (table) { - // passing `true` as the second arg tells it to - // reset the paging - table.ajax.reload(null, true); - } - }); + // Build the table. We rerun this only when the table + // options specified by the user changes, which should be + // only when the `table` attribute changes reactively. + template.autorun((c) => { + const userOptions = template.tabular.options.get(); + const options = _.extend({}, ajaxOptions, userOptions); + + //console.log('userOptions autorun', userOptions); + + // unless the user provides her own displayStart, + // we use a value from Session. This keeps the + // same page selected after a hot code push. + if (c.firstRun && !('displayStart' in options)) { + options.displayStart = Tracker.nonreactive(function () { + return Session.get('Tabular.LastSkip'); + }); + } - template.autorun(() => { - // these 5 are the parameters passed to "tabular_getInfo" subscription - // so when they *change*, set the isLoading flag to true - template.tabular.tableName.get(); - template.tabular.pubSelector.get(); - template.tabular.sort.get(); - template.tabular.skip.get(); - template.tabular.limit.get(); - template.tabular.isLoading.set(true); - template.tabular.searchTerm.get(); - }); + if (!('order' in options)) { + options.order = []; + } - // First Subscription - // Subscribe to an array of _ids that should be on the - // current page of the table, plus some aggregate - // numbers that DataTables needs in order to show the paging. - // The server will reactively keep this info accurate. - // It's not necessary to call stop - // on subscriptions that are within autorun computations. - template.autorun(function () { - if (!template.tabular.ready.get()) { - return; - } + // After the first time, we need to destroy before rebuilding. + if (table) { - //console.log('tabular_getInfo autorun'); + let dt = createDatatable(template); + if (dt) { + dt.destroy(); + } + template.$tableElement.empty(); + } - function onReady() { - template.tabular.isLoading.set(false); - } + // We start with an empty table. + // Data will be populated by ajax function now. + table = createDatatable(template, options); - let connection = template.tabular.connection; - let context = connection || Meteor; - context.subscribe( - 'tabular_getInfo', - template.tabular.tableName.get(), - template.tabular.pubSelector.get(), - template.tabular.sort.get(), - template.tabular.skip.get(), - template.tabular.limit.get(), - template.tabular.searchTerm.get(), - onReady - ); - }); + if (options.buttonContainer) { + const container = $(options.buttonContainer, table.table().container()); + table.buttons().container().appendTo(container); + } + }); - // Second Subscription - // Reactively subscribe to the documents with _ids given to us. Limit the - // fields to only those we need to display. It's not necessary to call stop - // on subscriptions that are within autorun computations. - template.autorun(function () { - // tableInfo is reactive and causes a rerun whenever the - // list of docs that should currently be in the table changes. - // It does not cause reruns based on the documents themselves - // changing. - let tableName = template.tabular.tableName.get(); - let collection = template.tabular.collection.get(); - let tableInfo = Tabular.getRecord(tableName, collection) || {}; - - //console.log('tableName and tableInfo autorun', tableName, tableInfo); - - template.tabular.recordsTotal = tableInfo.recordsTotal || 0; - template.tabular.recordsFiltered = tableInfo.recordsFiltered || 0; - - // In some cases, there is no point in subscribing to nothing - if ( - _.isEmpty(tableInfo) || - template.tabular.recordsTotal === 0 || - template.tabular.recordsFiltered === 0 - ) { - return; - } + template.autorun(() => { + // Get table name non-reactively + let tableName = Tracker.nonreactive(function () { + return template.tabular.tableName.get(); + }); + // Get the collection that we're showing in the table non-reactively + let collection = Tracker.nonreactive(function () { + return template.tabular.collection.get(); + }); - // Extend with extraFields from table definition - let fields = template.tabular.fields; - if (fields) { - // Extend with extraFields from table definition - if (typeof template.tabular.tableDef.extraFields === 'object') { - fields = _.extend(_.clone(fields), template.tabular.tableDef.extraFields); + // React when the requested list of records changes. + // This can happen for various reasons. + // * DataTables reran ajax due to sort changing. + // * DataTables reran ajax due to page changing. + // * DataTables reran ajax due to results-per-page changing. + // * DataTables reran ajax due to search terms changing. + // * `selector` attribute changed reactively + // * Docs were added/changed/removed by this user or + // another user, causing visible result set to change. + let tableInfo = Tabular.getRecord(tableName, collection); + if (!collection || !tableInfo) { + return; } - } - template.tabular.tableDef.sub.subscribe( - template.tabular.docPub.get(), - tableName, - tableInfo.ids || [], - fields - ); - }); + // Build options object to pass to `find`. + // It's important that we use the same options + // that were used in generating the list of `_id`s + // on the server. + let findOptions = {}; + let fields = template.tabular.fields; + if (fields) { + // Extend with extraFields from table definition + if (typeof template.tabular.tableDef.extraFields === 'object') { + _.extend(fields, template.tabular.tableDef.extraFields); + } + findOptions.fields = fields; + } - // Build the table. We rerun this only when the table - // options specified by the user changes, which should be - // only when the `table` attribute changes reactively. - template.autorun((c) => { - const userOptions = template.tabular.options.get(); - const options = _.extend({}, ajaxOptions, userOptions); - - //console.log('userOptions autorun', userOptions); - - // unless the user provides her own displayStart, - // we use a value from Session. This keeps the - // same page selected after a hot code push. - if (c.firstRun && !('displayStart' in options)) { - options.displayStart = Tracker.nonreactive(function () { - return Session.get('Tabular.LastSkip'); + // Sort does not need to be reactive here; using + // reactive sort would result in extra rerunning. + let sort = Tracker.nonreactive(function () { + return template.tabular.sort.get(); }); - } - - if (!('order' in options)) { - options.order = []; - } - - // After the first time, we need to destroy before rebuilding. - if (table) { - let dt = template.$tableElement.DataTable(); - if (dt) { - dt.destroy(); + if (sort) { + findOptions.sort = sort; } - template.$tableElement.empty(); - } - // We start with an empty table. - // Data will be populated by ajax function now. - table = template.$tableElement.DataTable(options); + // Get the updated list of docs we should be showing + let cursor = collection.find({ _id: { $in: tableInfo.ids } }, findOptions); + + //console.log('tableInfo, fields, sort, find autorun', cursor.count()); + //console.log( 'autorun: cursor.count', cursor.count(), 'tableInfo.ids.length', tableInfo.ids.length ); + + // We're subscribing to the docs just in time, so there's + // a good chance that they aren't all sent to the client yet. + // We'll stop here if we didn't find all the docs we asked for. + // This will rerun one or more times as the docs are received + // from the server, and eventually we'll have them all. + // Without this check in here, there's a lot of flashing in the + // table as rows are added. + if (cursor.count() < tableInfo.ids.length) { + return; + } + // Get data as array for DataTables to consume in the ajax function + template.tabular.data = cursor.fetch(); + + if (template.tabular.blazeViews) { + //console.log(`Removing ${template.blazeViews.length}`); + template.tabular.blazeViews.forEach(view => { + try { + Blaze.remove(view); + } catch (err) { + console.error(err); + } + }); + template.tabular.blazeViews = []; + } - if (options.buttonContainer) { - const container = $(options.buttonContainer, table.table().container()); - table.buttons().container().appendTo(container); - } - }); + // For these types of reactive changes, we don't want to + // reset the page we're on, so we pass `false` as second arg. + // The exception is if we changed the results-per-page number, + // in which cases `resetTablePaging` will be `true` and we will do so. + if (table) { + if (resetTablePaging) { + table.ajax.reload(null, true); + resetTablePaging = false; + } + else { + table.ajax.reload(null, false); + } + } - template.autorun(() => { - // Get table name non-reactively - let tableName = Tracker.nonreactive(function () { - return template.tabular.tableName.get(); - }); - // Get the collection that we're showing in the table non-reactively - let collection = Tracker.nonreactive(function () { - return template.tabular.collection.get(); + template.tabular.isLoading.set(false); }); - // React when the requested list of records changes. - // This can happen for various reasons. - // * DataTables reran ajax due to sort changing. - // * DataTables reran ajax due to page changing. - // * DataTables reran ajax due to results-per-page changing. - // * DataTables reran ajax due to search terms changing. - // * `selector` attribute changed reactively - // * Docs were added/changed/removed by this user or - // another user, causing visible result set to change. - let tableInfo = Tabular.getRecord(tableName, collection); - if (!collection || !tableInfo) { - return; - } - - // Build options object to pass to `find`. - // It's important that we use the same options - // that were used in generating the list of `_id`s - // on the server. - let findOptions = {}; - let fields = template.tabular.fields; - if (fields) { - // Extend with extraFields from table definition - if (typeof template.tabular.tableDef.extraFields === 'object') { - _.extend(fields, template.tabular.tableDef.extraFields); + template.autorun(() => { + const isLoading = template.tabular.isLoading.get(); + if (isLoading) { + template.$('.dataTables_processing').show(); } - findOptions.fields = fields; - } + else { + template.$('.dataTables_processing').hide(); + } + }); - // Sort does not need to be reactive here; using - // reactive sort would result in extra rerunning. - let sort = Tracker.nonreactive(function () { - return template.tabular.sort.get(); + // force table paging to reset to first page when we change page length + template.$tableElement.on('length.dt', function () { + resetTablePaging = true; }); - if (sort) { - findOptions.sort = sort; - } + }); - // Get the updated list of docs we should be showing - let cursor = collection.find({ _id: { $in: tableInfo.ids } }, findOptions); - - //console.log('tableInfo, fields, sort, find autorun', cursor.count()); - //console.log( 'autorun: cursor.count', cursor.count(), 'tableInfo.ids.length', tableInfo.ids.length ); - - // We're subscribing to the docs just in time, so there's - // a good chance that they aren't all sent to the client yet. - // We'll stop here if we didn't find all the docs we asked for. - // This will rerun one or more times as the docs are received - // from the server, and eventually we'll have them all. - // Without this check in here, there's a lot of flashing in the - // table as rows are added. - if (cursor.count() < tableInfo.ids.length) { - return; + Template.tabular.onDestroyed(function () { + // Clear last skip tracking + Session.set('Tabular.LastSkip', 0); + // Run a user-provided onUnload function + if ( + this.tabular && + this.tabular.tableDef && + typeof this.tabular.tableDef.onUnload === 'function' + ) { + this.tabular.tableDef.onUnload(); } - // Get data as array for DataTables to consume in the ajax function - template.tabular.data = cursor.fetch(); - if (template.tabular.blazeViews) { - //console.log(`Removing ${template.blazeViews.length}`); - template.tabular.blazeViews.forEach(view => { + if (this.tabular?.blazeViews) { + //console.log(`Removing ${this.blazeViews.length}`); + this.tabular.blazeViews.forEach(view => { try { Blaze.remove(view); - } - catch(err) { + } catch (err) { console.error(err); } }); - template.tabular.blazeViews = []; + this.tabular.blazeViews = []; } - // For these types of reactive changes, we don't want to - // reset the page we're on, so we pass `false` as second arg. - // The exception is if we changed the results-per-page number, - // in which cases `resetTablePaging` will be `true` and we will do so. - if (table) { - if (resetTablePaging) { - table.ajax.reload(null, true); - resetTablePaging = false; - } else { - table.ajax.reload(null, false); + // Destroy the DataTable instance to avoid memory leak + if (this.$tableElement && this.$tableElement.length) { + const dt = createDatatable(this); + if (dt) { + dt.destroy(); } - } - - template.tabular.isLoading.set(false); - }); - - template.autorun(() => { - const isLoading = template.tabular.isLoading.get(); - if (isLoading) { - template.$('.dataTables_processing').show(); - } else { - template.$('.dataTables_processing').hide(); + this.$tableElement.empty(); } }); - // force table paging to reset to first page when we change page length - template.$tableElement.on('length.dt', function () { - resetTablePaging = true; - }); -}); - -Template.tabular.onDestroyed(function () { - // Clear last skip tracking - Session.set('Tabular.LastSkip', 0); - // Run a user-provided onUnload function - if ( - this.tabular && - this.tabular.tableDef && - typeof this.tabular.tableDef.onUnload === 'function' - ) { - this.tabular.tableDef.onUnload(); - } - - if (this.tabular?.blazeViews) { - //console.log(`Removing ${this.blazeViews.length}`); - this.tabular.blazeViews.forEach(view => { - try { - Blaze.remove(view); - } - catch(err) { - console.error(err); - } - }); - this.tabular.blazeViews = []; - } - - // Destroy the DataTable instance to avoid memory leak - if (this.$tableElement && this.$tableElement.length) { - const dt = this.$tableElement.DataTable(); - if (dt) { - dt.destroy(); - } - this.$tableElement.empty(); - } -}); - //function setUpTestingAutoRunLogging(template) { // template.autorun(function () { // var val = template.tabular.tableName.get(); @@ -579,5 +593,5 @@ Template.tabular.onDestroyed(function () { // console.log('limit changed', val); // }); //} - +} export default Tabular; diff --git a/common/Tabular.js b/common/Tabular.js index 5eb0fca..315724e 100644 --- a/common/Tabular.js +++ b/common/Tabular.js @@ -79,4 +79,9 @@ Tabular.Table = class { } }; +/** + * Does nothing, used to keep Isomorphic with client. + */ +Tabular.init = () => {} + export default Tabular; diff --git a/package.js b/package.js index 1a74f41..3b98416 100755 --- a/package.js +++ b/package.js @@ -3,22 +3,19 @@ Package.describe({ name: 'aldeed:tabular', summary: 'Datatables for large or small datasets in Meteor', - version: '3.0.0-rc.0', + version: '3.0.0-rc.4', git: 'https://github.com/Meteor-Community-Packages/meteor-tabular.git' }); -Npm.depends({ - 'datatables.net': '2.0.8' -}); Package.onUse(function(api) { - api.versionsFrom([ '1.3', '2.8.0', '3.0-rc.4']); + api.versionsFrom([ '1.3', '2.8.0', '3.0']); api.use([ 'check', 'ecmascript', 'underscore', 'mongo', - 'blaze@2.9.0 || 3.0.0-rc300.2', + 'blaze@2.9.0 || 3.0.0', 'templating', 'reactive-var', 'tracker', @@ -28,7 +25,7 @@ Package.onUse(function(api) { // jquery is a weak reference in case you want to use a different package or // pull it in another way, but regardless you need to make sure it is loaded // before any tabular tables are rendered - api.use(['jquery@1.1.6 || 3.0.0 || 3.0.1-alpha300.10'], 'client', {weak: true}); + api.use(['jquery@1.1.6 || 3.0.0'], 'client', {weak: true}); api.use(['meteorhacks:subs-manager@1.2.0'], ['client', 'server'], {weak: true}); @@ -48,7 +45,7 @@ Package.onUse(function(api) { }); Package.onTest(function(api) { - api.versionsFrom([ '1.3', '2.8.0', '3.0-rc.4']); + api.versionsFrom([ '1.3', '2.8.0', '3.0']); api.use(['aldeed:tabular', 'tinytest']); api.use([ 'anti:fake', diff --git a/server/main.js b/server/main.js index 8aa6d89..cb242c3 100755 --- a/server/main.js +++ b/server/main.js @@ -72,7 +72,7 @@ Meteor.publish('tabular_getInfo', async function (tableName, selector, sort, ski // from this function, with sensitive data, there is // a chance someone could do a query and learn something // just based on whether a result is found or not. - if (typeof table.allow === 'function' && !table.allow(this.userId)) { + if (typeof table.allow === 'function' && !(await table.allow(this.userId))) { this.ready(); return; } @@ -81,7 +81,7 @@ Meteor.publish('tabular_getInfo', async function (tableName, selector, sort, ski // Allow the user to modify the selector before we use it if (typeof table.changeSelector === 'function') { - newSelector = table.changeSelector(newSelector, this.userId); + newSelector = await table.changeSelector(newSelector, this.userId); } // Apply the server side selector specified in the tabular @@ -89,7 +89,7 @@ Meteor.publish('tabular_getInfo', async function (tableName, selector, sort, ski // them using $and, allowing both selectors to have // the same keys. if (typeof table.selector === 'function') { - const tableSelector = table.selector(this.userId); + const tableSelector = await table.selector(this.userId); if (_.isEmpty(newSelector)) { newSelector = tableSelector; } else { @@ -124,7 +124,7 @@ Meteor.publish('tabular_getInfo', async function (tableName, selector, sort, ski const paths = getSearchPaths(table); const newSort = transformSortArray(findOptions.sort); - filteredRecordIds = table.searchCustom( + filteredRecordIds = await table.searchCustom( this.userId, newSelector, tokens, @@ -197,7 +197,7 @@ Meteor.publish('tabular_getInfo', async function (tableName, selector, sort, ski // Handle docs being added or removed from the result set. let initializing = true; - const handle = filteredCursor?.observeChanges({ + const handle = filteredCursor?.observeChangesAsync({ added: function (id) { if (initializing) { return;