Fork me on GitHub

Filter and Pagination

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

Currently we are dealing with only a few parties, but we also need to support a large number of parties.

Therefore, we want to have pagination support.

With pagination we can break the array of parties down to pages so the user won't have to scroll down to find a party, but also, and even more importantly, we can fetch only a few parties at a time instead of the entire parties collection for better performance.

The interesting thing about pagination is that it is dependent on the filters we want to put on top of the collection. For example, if we are in page 3, but we change how we sort the collection, we should get different results. Same thing with search: if we start a search, there might not be enough results for 3 pages.

For Angular 1 developers this chapter will show how powerful Meteor is. In the official Angular 1 tutorial, we added sorting and search that only worked on the client side, which in real world scenarios is not very helpful. Now, in this chapter we are going to perform a real-time search, sort and paginate that will run all the way to the server.

angular-meteor pagination support

What we want to achieve with angular-meteor is server-based reactive pagination. That is no simple task, but using angular-meteor could make life a lot simpler.

To achieve server-based reactive pagination we need to have support for pagination on the server as well as on the client. This means that our publish function for the parties collection would have to change and so does the way that we subscribe to that publication. So first let's take care of our server-side:

In our publish.js file in the parties directory we are going to add the options variable to the publish method like this:

12.1 Add options to the parties publish imports/api/parties/publish.js
3
4
5
6
7
8
9
 
26
27
28
29
30
31
import { Parties } from './collection';
 
if (Meteor.isServer) {
  Meteor.publish('parties', function(options) {
    const selector = {
      $or: [{
        // the public parties
...some lines skipped...
      }]
    };
 
    return Parties.find(selector, options);
  });
}

Now our publish method receives an options argument which we then pass to the Parties.find() function call. This will allow us to send arguments to the find function's modifier right from the subscribe call. The options object can contain properties like skip, sort and limit which we will shortly use ourselves - Collection Find.

Let's get back to our client code. We now need to change our subscribe call with options we want to set for pagination. What are those parameters that we want to set on the options argument? In order to have pagination in our parties list we will need to save the current page, the number of parties per page and the sort order. So let's add these parameters to our PartiesList component.

We will perPage, page and sort variables will later effect our subscription and we want the subscription to re-run every time one of them changes.

12.2 Add the pagination default params imports/ui/components/partiesList/partiesList.js
13
14
15
16
17
18
19
20
21
22
23
24
 
    $reactive(this).attach($scope);
 
    this.perPage = 3;
    this.page = 1;
    this.sort = {
      name: 1
    };
 
    this.subscribe('parties');
 
    this.helpers({

Right now, we just use subscribe without any parameters, but we need to provide some arguments to the subscriptions.

In order to do that, we will add a second parameter to the subscribe method, and we will provide a function that returns an array of arguments for the subscription.

We will use getReactively in order to get the current value, and this will also make them Reactive variables, and every change of them will effect the subscription parameters.

12.3 Add the params to the subscription imports/ui/components/partiesList/partiesList.js
19
20
21
22
23
24
25
26
27
28
29
      name: 1
    };
 
    this.subscribe('parties', () => [{
      limit: parseInt(this.perPage),
      skip: parseInt((this.getReactively('page') - 1) * this.perPage),
      sort: this.getReactively('sort')}
    ]);
 
    this.helpers({
      parties() {

That means that this.page and this.sort are now reactive and Meteor will re-run the subscription every time one of them will change.

Now we've built an object that contains 3 properties:

  • limit - how many parties to send per page
  • skip - the number of parties we want to start with, which is the current page minus one, times the parties per page
  • sort - the sorting of the collection in MongoDB syntax

Now we also need to add the sort modifier to the way we get the collection data from the Minimongo. That is because the sorting is not saved when the data is sent from the server to the client. So to make sure our data is also sorted on the client, we need to define it again in the parties collection.

To do that we are going to add the sort variable, and use it again with getReactively, in order to run the helper each time the sort changes:

12.4 Add the sort parameter to the collection helper imports/ui/components/partiesList/partiesList.js
27
28
29
30
31
32
33
34
35
 
    this.helpers({
      parties() {
        return Parties.find({}, {
          sort : this.getReactively('sort')
        });
      }
    });
  }

pagination directive

Now we need a UI to change pages and move between them.

In Angular 1's eco system there are a lot of directives for handling pagination.

Our personal favorite is angular-utils-pagination.

To add the directive add its Meteor package to the project:

meteor npm install --save angular-utils-pagination

Add it as a dependency to our Socially app:

1
2
3
4
5
6
7
 
42
43
44
45
46
47
48
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import uiRouter from 'angular-ui-router';
import utilsPagination from 'angular-utils-pagination';
 
import template from './partiesList.html';
import { Parties } from '../../../api/parties';
...some lines skipped...
export default angular.module(name, [
  angularMeteor,
  uiRouter,
  utilsPagination,
  PartyAdd,
  PartyRemove
]).component(name, {

Now let's add the directive in PartiesList, change the ng-repeat of parties to this:

12.7 Add usage to the dir-pagination directive imports/ui/components/partiesList/partiesList.html
1
2
3
4
5
6
7
<party-add></party-add>
 
<ul>
  <li dir-paginate="party in partiesList.parties | itemsPerPage: partiesList.perPage" total-items="partiesList.partiesCount">
    <a ui-sref="partyDetails({ partyId: party._id })">
      {{party.name}}
    </a>

and after the UL closes, add this directive:

12.8 Add the pagination controls to the view imports/ui/components/partiesList/partiesList.html
9
10
11
12
13
    <party-remove party="party"></party-remove>
  </li>
</ul>
 
<dir-pagination-controls on-page-change="partiesList.pageChanged(newPageNumber)"></dir-pagination-controls>

As you can see, dir-paginate list takes the number of objects in a page (that we defined before) but also takes the total number of items (we will get to that soon). With this binding it calculates which page buttons it should display inside the dir-pagination-controls directive.

On the dir-pagination-controls directive there is a method on-page-change and there we can call our own function.

So we call the pageChanged function with the new selection as a parameter.

Let's create the pageChanged function inside the PartiesList component:

12.9 Add the pageChanged method to the component imports/ui/components/partiesList/partiesList.js
34
35
36
37
38
39
40
41
42
43
      }
    });
  }
 
  pageChanged(newPage) {
    this.page = newPage;
  }
}
 
const name = 'partiesList';

Now every time we change the page, the scope variable will change accordingly and update the bind method that watches it.

  • Note that, at this point, the pagination will not work until we add the missing partiesCount variable in the next step of the tutorial.

Getting the total count of a collection

Getting a total count of a collection might seem easy, but there is a problem: The client only holds the number of objects that it subscribed to. This means that, if the client is not subscribed to the whole array, calling find().count on a collection will result in a partial count.

So we need access on the client to the total count even if we are not subscribed to the whole collection.

For that we can use the tmeasday:publish-counts package. On the command line:

meteor add tmeasday:publish-counts

This package helps to publish the count of a cursor in real-time, without any dependency on the subscribe method.

Inside the imports/api/parties/publish.js file, add the code that handles the count inside the Meteor.publish('parties') function, at the beginning of the function, before the existing return statement. So the file should look like this now:

12.11 Add usage of Counts imports/api/parties/publish.js
1
2
3
4
5
 
27
28
29
30
31
32
33
34
35
36
import { Meteor } from 'meteor/meteor';
import { Counts } from 'meteor/tmeasday:publish-counts';
 
import { Parties } from './collection';
 
...some lines skipped...
      }]
    };
 
    Counts.publish(this, 'numberOfParties', Parties.find(selector), {
      noReady: true
    });
 
    return Parties.find(selector, options);
  });
}

As you can see, we query only the parties that should be available to that specific client, but without the options variable so we get the full number of parties.

  • We are passing { noReady: true } in the last argument so that the publication will be ready only after our main cursor is ready - readiness.

With this, we have access to the Counts collection from our client.

So let's create another helper that uses Counts:

12.12 Add the Count usage in the component imports/ui/components/partiesList/partiesList.js
3
4
5
6
7
8
9
10
 
33
34
35
36
37
38
39
40
41
import uiRouter from 'angular-ui-router';
import utilsPagination from 'angular-utils-pagination';
 
import { Counts } from 'meteor/tmeasday:publish-counts';
 
import template from './partiesList.html';
import { Parties } from '../../../api/parties';
import { name as PartyAdd } from '../partyAdd/partyAdd';
...some lines skipped...
        return Parties.find({}, {
          sort : this.getReactively('sort')
        });
      },
      partiesCount() {
        return Counts.get('numberOfParties');
      }
    });
  }

Now the partiesCount will hold the number of parties and will send it to the directive in PartiesList (which we've already defined earlier).

Reactive variables

Meteor is relying deeply on the concept of reactivity.

This means that, when a reactive variable changes, Meteor is made aware of it via its Tracker object.

But Angular's scope variables are only watched by Angular and are not reactive vars for Meteor...

For that, angular-meteor provides the helpers, and each time you will defined a variable, a new ReactiveVar will be created and will cause the Tracker to update all the subscriptions!

Changing the sort order reactively

We haven't placed a way to change sorting anywhere in the UI, so let's do that right now.

Create template for the new Component called PartiesSort:

12.13 Create view for PartiesSort component imports/ui/components/partiesSort/partiesSort.html
1
2
3
4
5
6
<div>
  <select ng-model="partiesSort.order" ng-change="partiesSort.changed()">
    <option value="1">Ascending</option>
    <option value="-1">Descending</option>
  </select>
</div>

Now we can create actual component:

12.14 Create PartiesSort component imports/ui/components/partiesSort/partiesSort.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import angular from 'angular';
import angularMeteor from 'angular-meteor';
 
import template from './partiesSort.html';
 
class PartiesSort {
  constructor($timeout) {
    'ngInject';
 
    $timeout(() => this.changed());
  }
 
  changed() {
    this.onChange({
      sort: {
        [this.property]: parseInt(this.order)
      }
    });
  }
}
 
const name = 'partiesSort';
 
// create a module
export default angular.module(name, [
  angularMeteor
]).component(name, {
  template,
  bindings: {
    onChange: '&',
    property: '@',
    order: '@'
  },
  controllerAs: name,
  controller: PartiesSort
});

Binding? Methods? Don't worry. Let me explain it.

PartiesSort uses 3 bindings:

  • onChange - an expression that is called on every sort change
  • property - a value with field's name that will be used in sorting
  • order - a value with default order (1 or -1)

As you can see there is a changed() method. It is called when user changes sorting order. It calls onChange expression with one argument wich is the sort object. It contains just one property (you can expend it in the future!) with name of the sorted field as a key and the order as a value.

Let's now add our component to the PartiesList:

7
8
9
10
11
12
13
 
53
54
55
56
57
58
59
 
import template from './partiesList.html';
import { Parties } from '../../../api/parties';
import { name as PartiesSort } from '../partiesSort/partiesSort';
import { name as PartyAdd } from '../partyAdd/partyAdd';
import { name as PartyRemove } from '../partyRemove/partyRemove';
 
...some lines skipped...
  angularMeteor,
  uiRouter,
  utilsPagination,
  PartiesSort,
  PartyAdd,
  PartyRemove
]).component(name, {
1
2
3
4
5
6
7
<party-add></party-add>
 
<parties-sort on-change="partiesList.sortChanged(sort)" property="name" order="1"></parties-sort>
 
<ul>
  <li dir-paginate="party in partiesList.parties | itemsPerPage: partiesList.perPage" total-items="partiesList.partiesCount">
    <a ui-sref="partyDetails({ partyId: party._id })">

We made PartiesSort to use name field with ASC order by default. We also added onChange expression. It is just to handle changes.

44
45
46
47
48
49
50
51
52
53
  pageChanged(newPage) {
    this.page = newPage;
  }
 
  sortChanged(sort) {
    this.sort = sort;
  }
}
 
const name = 'partiesList';

And we don't have to do anything other than that, because we defined sort variable as helper, and when we will change it, Angular-Meteor will take care of updating the subscription for us.

So all we have left is to sit back and enjoy our pagination working like a charm.

We've made a lot of changes, so please check the step's code here to make sure you have everything needed and can run the application.

Reactive Search

Now that we have the basis for pagination, all we have left to do is add reactive full stack searching of parties. This means that we will be able to enter a search string, have the app search for parties that match that name in the server and return only the relevant results! This is pretty awesome, and we are going to do all that in only a few lines of code. So let's get started!

As before, let's add the server-side support. We need to add a new argument to our publish method which will hold the requested search string. We will call it... searchString! Here it goes:

12.18 Add searchString to `parties` publication imports/api/parties/publish.js
4
5
6
7
8
9
10
 
27
28
29
30
31
32
33
34
35
36
37
38
39
import { Parties } from './collection';
 
if (Meteor.isServer) {
  Meteor.publish('parties', function(options, searchString) {
    const selector = {
      $or: [{
        // the public parties
...some lines skipped...
      }]
    };
 
    if (typeof searchString === 'string' && searchString.length) {
      selector.name = {
        $regex: `.*${searchString}.*`,
        $options : 'i'
      };
    }
 
    Counts.publish(this, 'numberOfParties', Parties.find(selector), {
      noReady: true
    });

Now we are going to filter the correct results using mongo's regex ability. We are going to add this line in those two places where we are using find: in publish Counts and in the return of the parties cursor:

'name' : { '$regex' : `.*${searchString}.*`, '$options' : 'i' },

As you can see, this will filter all the parties whose name contains the searchString.

We added also if (typeof searchString === 'string' && searchString.length) so that, if we don't get that parameter, we will just return the whole collection.

Now let's move on to the client-side. First let's place a search input into our template and bind it to a 'searchText' component variable:

12.20 Add input with searchText as ngModel imports/ui/components/partiesList/partiesList.html
1
2
3
4
5
6
7
<party-add></party-add>
 
<input type="search" ng-model="partiesList.searchText" placeholder="Search" />
 
<parties-sort on-change="partiesList.sortChanged(sort)" property="name" order="1"></parties-sort>
 
<ul>

And all we have left to do is call the subscribe method with our reactive variable, and add the searchText as reactive helper:

12.19 Add searchText to subscription imports/ui/components/partiesList/partiesList.js
22
23
24
25
26
27
28
29
30
31
32
33
34
    this.sort = {
      name: 1
    };
    this.searchText = '';
 
    this.subscribe('parties', () => [{
        limit: parseInt(this.perPage),
        skip: parseInt((this.getReactively('page') - 1) * this.perPage),
        sort: this.getReactively('sort')
      }, this.getReactively('searchText')
    ]);
 
    this.helpers({

Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { name as PartiesSort } from '../partiesSort';
import 'angular-mocks';
 
describe('PartiesSort', () => {
  beforeEach(() => {
    window.module(PartiesSort);
  });
 
  describe('controller', () => {
    let controller;
    const onChange = function() {};
    const property = 'name';
    const order = -1;
 
 
    beforeEach(() => {
      inject(($rootScope, $componentController) => {
        controller = $componentController(PartiesSort, {
          $scope: $rootScope.$new(true)
        }, {
          onChange,
          property,
          order
        });
      });
    });
 
    it('should set property', () => {
      expect(controller.property).toEqual(property);
    });
 
    it('should set order', () => {
      expect(controller.order).toEqual(order);
    });
 
    it('should set onChange', () => {
      expect(controller.onChange).toBe(onChange);
    });
 
    describe('changed()', () => {
      it('should call onChange expression', () => {
        spyOn(controller, 'onChange');
 
        controller.changed();
 
        expect(controller.onChange).toHaveBeenCalledWith({
          sort: {
            [property]: order
          }
        });
      });
    });
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { name as PartiesList } from '../partiesList';
import 'angular-mocks';
 
describe('PartiesList', () => {
  beforeEach(() => {
    window.module(PartiesList);
  });
 
  describe('controller', () => {
    let controller;
 
    beforeEach(() => {
      inject(($rootScope, $componentController) => {
        controller = $componentController(PartiesList, {
          $scope: $rootScope.$new(true)
        });
      });
    });
 
    it('should have perPage that equals 3 by default', () => {
      expect(controller.perPage).toEqual(3);
    });
 
    it('should have page that equals 1 by default', () => {
      expect(controller.page).toEqual(1);
    });
 
    it('should sort by name - ASC', () => {
      expect(controller.sort).toEqual({
        name: 1
      });
    });
 
    it('should be able to change sorting', () => {
      controller.sortChanged({
        name: -1
      });
 
      expect(controller.sort).toEqual({
        name: -1
      });
    });
 
    it('should be able to change page', () => {
      controller.pageChanged(2);
 
      expect(controller.page).toEqual(2);
    });
  });
});

Wow, that is all that's needed to have a fully reactive search with pagination! Quite amazing, right?

Summary

So now we have full pagination with search and sorting for client and server-side, with the help of Meteor's options and Angular 1's directives.