Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/reactive_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
</tr>
</thead>
<tbody>

{{#each sortedRows}}
<tr class="{{../rowClass this}}">
{{#each ../fields}}
Expand Down
171 changes: 105 additions & 66 deletions lib/reactive_table.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -311,6 +313,7 @@ var setup = function () {
context.reactiveTableSetup = true;

this.context = context;
currentContext.set(this);
};

var getDefaultFieldVisibility = function (field) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
72 changes: 68 additions & 4 deletions lib/sort.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
});
}
Expand Down Expand Up @@ -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<any[]>}
*/
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)
}
Loading