diff --git a/lib/reactive_table.html b/lib/reactive_table.html index 8f205ab..f0f105f 100644 --- a/lib/reactive_table.html +++ b/lib/reactive_table.html @@ -68,6 +68,7 @@ + {{#each sortedRows}} {{#each ../fields}} diff --git a/lib/reactive_table.js b/lib/reactive_table.js index ad46c0b..fa58809 100644 --- a/lib/reactive_table.js +++ b/lib/reactive_table.js @@ -1,5 +1,7 @@ var ReactiveTableCounts = new Mongo.Collection("reactive-table-counts"); +var currentContext = new ReactiveVar(undefined) + get = function(obj, field) { var keys = field.split('.'); var value = obj; @@ -311,6 +313,7 @@ var setup = function () { context.reactiveTableSetup = true; this.context = context; + currentContext.set(this); }; var getDefaultFieldVisibility = function (field) { @@ -336,34 +339,108 @@ var getPageCount = function () { return Math.ceil(count / rowsPerPage); }; -Template.reactiveTable.onCreated(function() { - this.updateHandle = _.debounce(updateHandle, 200); - - var rowsPerPage = this.data.rowsPerPage || (this.data.settings && this.data.settings.rowsPerPage); - var currentPage = this.data.currentPage || (this.data.settings && this.data.settings.currentPage); - var fields = this.data.fields || (this.data.settings && this.data.settings.fields) || []; - - var template = this; - Tracker.autorun(function(c) { - if (rowsPerPage instanceof ReactiveVar) { - rowsPerPage.dep.depend(); - } - if (currentPage instanceof ReactiveVar) { - currentPage.dep.depend(); - } - _.each(fields, function (field) { - if (field.sortOrder && field.sortOrder instanceof ReactiveVar) { - field.sortOrder.dep.depend(); - } - if (field.sortDirection && field.sortDirection instanceof ReactiveVar) { - field.sortDirection.dep.depend(); - } - }); - if (template.context) { - template.updateHandle(template.context); - } - }); -}); +/** + * Note: I included TemplateController, to have a state var, because in the html, I could + * not access async #let to manage a promise. Therefore, I put the old helper logic of + * sortedRows into an autorun and also included the old onCreated logic. + * Finally, the html does not recognise the state var, so that I created a helper passing the state var, + * this works. + */ +TemplateController('reactiveTable', { + state: { + sortedRows: [], + currentRows: [], + }, + onCreated() { + this.updateHandle = _.debounce(updateHandle, 200); + + var rowsPerPage = this.data.rowsPerPage || (this.data.settings && this.data.settings.rowsPerPage); + var currentPage = this.data.currentPage || (this.data.settings && this.data.settings.currentPage); + var fields = this.data.fields || (this.data.settings && this.data.settings.fields) || []; + + /** + * Pass a function(returnContext){} to the {{> ReactiveTable contextFn=functionHelper}} + * and set the context over the parameter. + */ + typeof this.data.contextFn === 'function' && this.data.contextFn(this) + + var template = this; + Tracker.autorun(function(c) { + if (rowsPerPage instanceof ReactiveVar) { + rowsPerPage.dep.depend(); + } + if (currentPage instanceof ReactiveVar) { + currentPage.dep.depend(); + } + _.each(fields, function (field) { + if (field.sortOrder && field.sortOrder instanceof ReactiveVar) { + field.sortOrder.dep.depend(); + } + if (field.sortDirection && field.sortDirection instanceof ReactiveVar) { + field.sortDirection.dep.depend(); + } + }); + if (template.context) { + template.updateHandle(template.context); + } + }); + + this.autorun(async (c) => { + if (!currentContext.get()) { + return + } + + const {context} = currentContext.get(); + + if (context.server) { + var sortedRows = this.publishedRows.find({ + "reactive-table-id": this.publicationId.get() + }, { + sort: { + "reactive-table-sort": 1 + } + }).fetch(); + this.state.currentRows = sortedRows + return sortedRows; + } else { + var sortByValue = _.all(getSortedFields(context.fields, context.multiColumnSort), function (field) { + return field.sortByValue || (!field.fn && !field.sortFn); + }); + var filterQuery = getFilterQuery(getFilterStrings(context.filters.get()), getFilterFields(context.filters.get(), context.fields), { + enableRegex: context.enableRegex, + filterOperator: context.filterOperator + }); + + var limit = context.rowsPerPage.get(); + var currentPage = context.currentPage.get(); + var skip = currentPage * limit; + + if (sortByValue) { + + var sortQuery = getSortQuery(context.fields, context.multiColumnSort); + var sortedRows = context.collection.find(filterQuery, { + sort: sortQuery, + skip: skip, + limit: limit + }).fetch(); + + this.state.currentRows = sortedRows + this.state.sortedRows = sortedRows; + } else { + var rows = context.collection.find(filterQuery).fetch(); + var sortedRows = await Tracker.withComputation(c, async () => await sortWithFunctions(rows, context.fields, context.multiColumnSort)); + sortedRows = sortedRows.slice(skip, skip + limit); + this.state.currentRows = sortedRows + this.state.sortedRows = sortedRows; + } + } + }) + }, helpers: { + sortedRows() { + return this.state.sortedRows + }, + }, +}) Template.reactiveTable.onDestroyed(function() { if (this.context.server && this.context.handle) { @@ -480,44 +557,6 @@ Template.reactiveTable.helpers({ return (sortDirection === 1); }, - 'sortedRows': function () { - if (this.server) { - return this.publishedRows.find({ - "reactive-table-id": this.publicationId.get() - }, { - sort: { - "reactive-table-sort": 1 - } - }); - } else { - var sortByValue = _.all(getSortedFields(this.fields, this.multiColumnSort), function (field) { - return field.sortByValue || (!field.fn && !field.sortFn); - }); - var filterQuery = getFilterQuery(getFilterStrings(this.filters.get()), getFilterFields(this.filters.get(), this.fields), {enableRegex: this.enableRegex, filterOperator: this.filterOperator}); - - var limit = this.rowsPerPage.get(); - var currentPage = this.currentPage.get(); - var skip = currentPage * limit; - - if (sortByValue) { - - var sortQuery = getSortQuery(this.fields, this.multiColumnSort); - return this.collection.find(filterQuery, { - sort: sortQuery, - skip: skip, - limit: limit - }); - - } else { - - var rows = this.collection.find(filterQuery).fetch(); - sortedRows = sortWithFunctions(rows, this.fields, this.multiColumnSort); - return sortedRows.slice(skip, skip + limit); - - } - } - }, - 'noData': function () { var pageCount = getPageCount.call(this); return (pageCount === 0) && this.noDataTmpl; diff --git a/lib/sort.js b/lib/sort.js index d23181c..3064629 100644 --- a/lib/sort.js +++ b/lib/sort.js @@ -80,19 +80,22 @@ getSortQuery = function (fields, multiColumnSort) { return sortQuery; }; -sortWithFunctions = function (rows, fields, multiColumnSort) { +sortWithFunctions = async function (rows, fields, multiColumnSort) { var sortedFields = getSortedFields(fields, multiColumnSort); var sortedRows = rows; - _.each(sortedFields.reverse(), function (field) { + await eachAsync(sortedFields.reverse(), async function (field) { if (field.sortFn) { - sortedRows = _.sortBy(sortedRows, function (row) { + sortedRows = await sortByAsync(sortedRows, function (row) { + // Supports async sortFn, too, sortFn should return a number, because its a good old sortfn return field.sortFn( get( row, field.key ), row ); }); } else if (field.sortByValue || !field.fn) { sortedRows = _.sortBy(sortedRows, field.key); } else { - sortedRows = _.sortBy(sortedRows, function (row) { + // This now can get async + sortedRows = await sortNameByAsync(sortedRows, function (row) { + // fn might return a promise return field.fn( get( row, field.key ), row ); }); } @@ -127,3 +130,64 @@ changePrimarySort = function(fieldId, fields, multiColumnSort) { }); } }; + +/** + * An async each which awaits all promises to be resolved before continuing. + * + * Iterates each value in object and performs the function provided + * + * Taken from async-lodash: https://github.com/sarunya/async-lodash/blob/master/lib/methods.js + * + * @param {Object} obj + * @param {*} fn + */ +async function eachAsync(obj, fn) { + for (const iKey in obj) { + // eslint-disable-next-line no-prototype-builtins + if (obj.hasOwnProperty(iKey)) { // Object.hasOwn is only available on node 16. + const val = obj[iKey] + await fn(val) + } + } +} + +/** + * Helper for being able to use async sort function in sortBy. + * @param arr + * @param asyncFn + * @returns {Promise} + */ +async function sortNameByAsync(arr, asyncFn) { + const promises = arr.map(async item => ({ + value: item, + sortKey: await asyncFn(item), + })) + + const resolved = await Promise.all(promises) + + resolved.sort((a, b) => a.sortKey.localeCompare(b.sortKey)) + + const result = resolved.map(item => item.value) + + return result +} + +/** + * Helper for being able to use async sort function in sortBy. + * + * @param arr - Elements to sort + * @param asyncFn - Sort function + * @returns {Promise<*>} - The array sorted in a Promise + */ +async function sortByAsync(arr, asyncFn) { + const promises = arr.map(async item => ({ + value: item, + sortKey: await asyncFn(item), + })) + + const resolved = await Promise.all(promises) + + resolved.sort((a, b) => a.sortKey - b.sortKey) + + return resolved.map(item => item.value) +} diff --git a/package.js b/package.js index 4ea1625..caf54da 100644 --- a/package.js +++ b/package.js @@ -5,8 +5,8 @@ Package.describe({ git: "https://github.com/aslagle/reactive-table.git" }); -Package.on_use(function (api) { - api.versionsFrom("METEOR@0.9.0"); +Package.onUse(function (api) { + api.versionsFrom("METEOR@2.8.0"); api.use('templating', 'client'); api.use('jquery', 'client'); api.use('underscore', ['server', 'client']); @@ -15,22 +15,24 @@ Package.on_use(function (api) { api.use("anti:i18n@0.4.3", 'client'); api.use("mongo@1.0.8", ["server", "client"]); api.use("check", "server"); + api.use('space:template-controller@0.3.0', 'client'); + api.use('blaze', 'client'); api.use("fortawesome:fontawesome@4.2.0", 'client', {weak: true}); - api.add_files('lib/reactive_table.html', 'client'); - api.add_files('lib/filter.html', 'client'); - api.add_files('lib/reactive_table_i18n.js', 'client'); - api.add_files('lib/reactive_table.js', 'client'); - api.add_files('lib/reactive_table.css', 'client'); - api.add_files('lib/sort.js', 'client'); - api.add_files('lib/filter.js', ['client', 'server']); - api.add_files('lib/server.js', 'server'); + api.addFiles('lib/reactive_table.html', 'client'); + api.addFiles('lib/filter.html', 'client'); + api.addFiles('lib/reactive_table_i18n.js', 'client'); + api.addFiles('lib/reactive_table.js', 'client'); + api.addFiles('lib/reactive_table.css', 'client'); + api.addFiles('lib/sort.js', 'client'); + api.addFiles('lib/filter.js', ['client', 'server']); + api.addFiles('lib/server.js', 'server'); api.export("ReactiveTable", ["client", "server"]); }); -Package.on_test(function (api) { +Package.onTest(function (api) { api.use('templating', 'client'); api.use('jquery', 'client'); api.use('underscore', ['client', 'server']); @@ -41,43 +43,43 @@ Package.on_test(function (api) { api.use("check", "server"); api.use("audit-argument-checks", "server"); - api.add_files('lib/reactive_table.html', 'client'); - api.add_files('lib/filter.html', 'client'); - api.add_files('lib/reactive_table_i18n.js', 'client'); - api.add_files('lib/reactive_table.js', 'client'); - api.add_files('lib/reactive_table.css', 'client'); - api.add_files('lib/sort.js', 'client'); - api.add_files('lib/filter.js', ['client', 'server']); - api.add_files('lib/server.js', 'server'); + api.addFiles('lib/reactive_table.html', 'client'); + api.addFiles('lib/filter.html', 'client'); + api.addFiles('lib/reactive_table_i18n.js', 'client'); + api.addFiles('lib/reactive_table.js', 'client'); + api.addFiles('lib/reactive_table.css', 'client'); + api.addFiles('lib/sort.js', 'client'); + api.addFiles('lib/filter.js', ['client', 'server']); + api.addFiles('lib/server.js', 'server'); api.export("ReactiveTable", ["client", "server"]); api.use(['tinytest', 'test-helpers'], 'client'); - api.add_files('test/helpers.js', ['client', 'server']); - api.add_files('test/test_collection_argument.js', 'client'); - api.add_files('test/test_no_data_template.html', 'client'); - api.add_files('test/test_settings.js', 'client'); - api.add_files('test/test_fields_tmpl.html', 'client'); - api.add_files('test/test_fields.js', 'client'); - + api.addFiles('test/helpers.js', ['client', 'server']); + api.addFiles('test/test_collection_argument.js', 'client'); + api.addFiles('test/test_no_data_template.html', 'client'); + api.addFiles('test/test_settings.js', 'client'); + api.addFiles('test/test_fields_tmpl.html', 'client'); + api.addFiles('test/test_fields.js', 'client'); + api.use('accounts-password@1.0.6', ['client', 'server']); - api.add_files('test/test_reactivity_server.js', 'server'); - api.add_files('test/test_reactivity.html', 'client'); - api.add_files('test/test_reactivity.js', 'client'); - - api.add_files('test/test_sorting.js', 'client'); - api.add_files('test/test_filtering_server.js', 'server'); - api.add_files('test/test_filtering.js', 'client'); - api.add_files('test/test_pagination.js', 'client'); - api.add_files('test/test_i18n.js', 'client'); - api.add_files('test/test_events_tmpl.html', 'client'); - api.add_files('test/test_events.js', 'client'); - api.add_files('test/test_column_toggles.js', 'client'); - api.add_files('test/test_multiple_tables.js', 'client'); - api.add_files('test/test_template.html', 'client'); - api.add_files('test/test_template.js', 'client'); - api.add_files('test/test_custom_filters.js', 'client'); + api.addFiles('test/test_reactivity_server.js', 'server'); + api.addFiles('test/test_reactivity.html', 'client'); + api.addFiles('test/test_reactivity.js', 'client'); + + api.addFiles('test/test_sorting.js', 'client'); + api.addFiles('test/test_filtering_server.js', 'server'); + api.addFiles('test/test_filtering.js', 'client'); + api.addFiles('test/test_pagination.js', 'client'); + api.addFiles('test/test_i18n.js', 'client'); + api.addFiles('test/test_events_tmpl.html', 'client'); + api.addFiles('test/test_events.js', 'client'); + api.addFiles('test/test_column_toggles.js', 'client'); + api.addFiles('test/test_multiple_tables.js', 'client'); + api.addFiles('test/test_template.html', 'client'); + api.addFiles('test/test_template.js', 'client'); + api.addFiles('test/test_custom_filters.js', 'client'); api.use("dburles:collection-helpers@1.0.1", "client"); - api.add_files("test/test_compatibility.js", "client"); + api.addFiles("test/test_compatibility.js", "client"); });