Non-infinite `paginated-subscription`

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>
                                            &nbsp;
                                            <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>