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");
});