Continuing the discussion from Collections in client are not defined: I discovered from that post about a package called percolate:paginated-subscription
I was hoping I can refactor my code to use that rather than build it myself, but from the README it states
TODO: Do actual “pagination” rather than “infinite scroll” – i.e. have an option to pass around an offset as well as limit.
So it does not do what I need it to do yet. I am just posting how I implemented pagination for now, using percolate:find-from-publication
and angular-smart-table
. FEEDBACK welcome, I just started Meteor and JavaScript server side programming in general less than couple of months ago and ES6 modules when Meteor 1.3 was released.
On the server side, I have defined this utility function that would define the reusable portions of the code. (You can refer to the previous version of this post to see the one-off version)
import { angular } from 'meteor/netanelgilad:polyfill-angular-server'
import { Mongo } from 'meteor/mongo' // eslint-disable-line no-unused-vars
import { Counts } from 'meteor/tmeasday:publish-counts'
import { FindFromPublication } from 'meteor/percolate:find-from-publication'
/**
* @callback selectorToSearchCb
* @param {object} selector
*/
/**
* Administrative list publication. This provides access to all the whole collection with pagination.
* Only allowed if the user is in the admin role.
*
* @param {string} publicationName publication name
* @param {Mongo.Collection} collection mongo collection
* @param (selectorToSearchCb) selectorToSearch selector to search function. This is used to convert input selectors to the search object for the find().
* @param {string} fields an array of field names that would be sent for edit and listing.
* @return {void}
*/
function PagedAdminPublication(publicationName, collection, selectorToSearch, ...fields) {
/**
* The count publication name. The publication name suffixed by '-count'
* @type {string}
*/
const countPublicationName = publicationName + '-count'
/**
* Map of fields. Find options require it to be a map.
* @type {Object}
*/
const FIELDS = fields.reduce((map, current)=> {
map[current] = true;
return map
}, {})
/**
* @param {object} selector a single ID represented as a string or a selector for mongo
* @param {object} options options for find
*/
FindFromPublication.publish(publicationName, function(selector, options) {
if (!Roles.userIsInRole(this.userId, ['admin']) || !selector) {
this.stop()
}
let search = selector;
if (!angular.isString(selector)) {
search = selectorToSearch(selector)
}
if (!options.limit || options.limit > 100) {
// do not return more than 100 records at a time
options.limit = 100
}
// only return desired fields for list and edit
options.fields = FIELDS
Counts.publish(this, countPublicationName, collection.find(search), {
noReady: true
});
return collection.find(search, options)
})
}
export default PagedAdminPublication
Also on the server side, I make use of the publication
import { Venues } from '/imports/collections'
import PagedAdminPublication from '/imports/admin-publication'
function selectorToSearch(selector) {
if (angular.isString(selector)) {
return selector
}
const search = {}
const predicates = []
if (selector.predicateObject) {
if (selector.predicateObject.name) {
predicates.push({
name: {
$regex: _.escapeRegExp(selector.predicateObject.name),
$options: 'i'
}
})
}
if (selector.predicateObject.contactFullName) {
predicates.push({
'contact.fullName': {
$regex: _.escapeRegExp(selector.predicateObject.contactFullName),
$options: 'i'
}
})
}
if (selector.predicateObject.contactEmail) {
predicates.push({
'contact.email': {
$regex: _.escapeRegExp(selector.predicateObject.contactEmail),
$options: 'i'
}
})
}
if (predicates.length > 0) {
search['$and'] = predicates
}
}
return search
}
PagedAdminPublication('venue-list-admin', Venues, selectorToSearch, 'name', 'contact', 'country', 'address', 'postalcode')
Client side JS. The SmartTableService
and st-persist
directives are used to store the last table state so I can recover it when I switch between UI router states.
import { Meteor } from 'meteor/meteor'
import { Counts } from 'meteor/tmeasday:publish-counts'
import { Venues } from '/imports/collections'
import angular from 'angular'
import module from './venues.module'
import './venues.tpl.html'
function updateByTableState(vm, tableState) {
vm.findSelector = tableState.search || {}
var direction = tableState.sort.reverse ? 'desc' : 'asc'
vm.skip = tableState.pagination.start
vm.sort = {
name: 1
}
vm.itemsByPage = tableState.pagination.number || 10
if (tableState.sort.predicate === 'name') {
vm.sort = [['name', direction]]
} else if (tableState.sort.predicate === 'contactFirstName') {
vm.sort = [['contact.sortFirstName', direction], ['contact.sortLastName', direction]]
} else if (tableState.sort.predicate === 'contactLastName') {
vm.sort = [['contact.sortLastName', direction], ['contact.sortFirstName', direction]]
} else if (tableState.sort.predicate === 'contactEmail') {
vm.sort = [['contact.email', direction], ['contact.email', direction]]
}
}
// Use the pattern of one state configuration per file.
module.config($stateProvider => {
$stateProvider.state('app.venues', {
url: '/venues',
data: {
title: 'Venues'
},
resolve: {
user: $auth => $auth.awaitUser(),
initialControllerState: function(SmartTableStateService, user) {
const initialTableState = {
search: {},
sort: {},
pagination: {
start: 0
}
}
const vm = {}
vm.itemsByPageOptions = [5, 10, 25, 50, 100]
updateByTableState(vm, angular.extend(initialTableState, SmartTableStateService.load('venues')))
return vm
}
},
views: {
'content@app': {
templateUrl: 'imports/client/venues/venues.tpl.html',
controllerAs: 'ctrl',
controller: function($reactive, $scope, $state, initialControllerState) {
var vm = this
angular.extend(vm, initialControllerState)
$reactive(vm).attach($scope)
vm.displayed = []
vm.subscribe('venue-list-admin', () => [vm.getReactively('findSelector', true), {
skip: vm.getReactively('skip'),
limit: vm.getReactively('itemsByPage'),
sort: vm.getReactively('sort')
}])
vm.subscribe('venue-list-admin-count', () => [vm.getReactively('findSelector', true)])
vm.helpers({
venues: () => Venues.findFromPublication('venue-list-admin', {}, {
sort: this.getReactively('sort')
}),
count: () => Counts.get('venue-list-admin-count')
})
vm.autorun(() => {
vm.displayed = vm.getReactively('venues', true)
vm.getReactively('count')
vm.getReactively('currentUser')
})
/**
* Called as part of st-pipe. This will convert tableState to the findSelector and findOptions.
* @param {object} tableState table state
* @return {void}
*/
vm.loadData = function(tableState) {
updateByTableState(vm, tableState)
const pagination = Object.assign({}, tableState.pagination)
const numberOfItems = vm.count
pagination.numberOfPages = Math.ceil(numberOfItems / pagination.number)
tableState.pagination = pagination
}
}
}
}
})
})
And finally the table
<table class="table table-bordered table-striped table-hover" st-table="ctrl.displayed"
st-pipe="ctrl.loadData" st-persist="venues">
<thead>
<tr>
<th style="width: 25%" st-sort="name" st-skip-natural="true">Name</th>
<th style="width: 25%" st-sort="contactFirstName" st-skip-natural="true">Contact
First Name
</th>
<th style="width: 25%" st-sort="contactLastName" st-skip-natural="true">Contact Last
Name
</th>
<th style="width: 25%" st-sort="contactEmail" st-skip-natural="true">Contact
E-mail
</th>
</tr>
<tr>
<th class="row">
<div class="input-group input-group-sm col-md-12">
<input st-search="name" placeholder="search for name"
class="form-control" type="search"/>
<span class="input-group-btn"><button st-clear-search="name"
class="btn btn-secondary"
type="button"><i
class="fa fa-eraser"> </i></button></span>
</div>
</th>
<th colspan="2" class="row">
<div class="input-group input-group-sm col-md-12">
<input st-search="contactFullName" placeholder="contact name search"
class="form-control"
type="search"/>
<span class="input-group-btn"><button st-clear-search="contactFullName"
class="btn btn-secondary"
type="button"><i
class="fa fa-eraser"> </i></button></span>
</div>
</th>
<th class="row">
<div class="input-group input-group-sm col-md-12">
<input st-search="contactEmail" placeholder="search for e-mail"
class="form-control" type="search" ng-model="searchEmail"/>
<span class="input-group-btn"><button st-clear-search="contactEmail"
class="btn btn-secondary"
type="button"><i
class="fa fa-eraser"> </i></button></span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in ctrl.displayed" ng-dblclick="ctrl.edit(item._id)">
<td>{{ item.name}} <i ng-if="ctrl.currentUser.venues.indexOf(item._id) != -1"
class="fa fa-building"> </i></td>
<td>{{ item.contact.firstName }}</td>
<td>{{ item.contact.lastName }}</td>
<td>{{ item.contact.email }}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">
<div class="row">
<div class="hidden-xs hidden-sm col-md-4 col-lg-6">
<div class="form-inline pagination">
<div class="form-group">
<label for="itemsByPage">Show</label>
<select class="form-control" id="itemsByPage"
ng-options="n for n in ctrl.itemsByPageOptions"
ng-model="ctrl.itemsByPage">
</select>
<label for="itemsByPage">entries per page of
<strong>{{ctrl.count}}</strong> entries.</label>
<span class="hidden-md"><i class="fa fa-building"> </i> indicates the venues associated with the current user.</span>
</div>
</div>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 col-lg-6 text-align-right">
<span st-pagination="" st-items-by-page="ctrl.itemsByPage"
st-displayed-pages="5"
st-template="client/pagination.tpl.html"></span>
<ul class="pagination">
<li>
<a ng-click="ctrl.add()" aria-label="Add">
<span aria-hidden="true"><i
class="fa fa-plus"></i> Add</span>
</a>
</li>
</ul>
</div>
</div>
</td>
</tr>
</tfoot>
</table>