Skip to content

Latest commit

Β 

History

History
882 lines (748 loc) Β· 34 KB

File metadata and controls

882 lines (748 loc) Β· 34 KB

Step 13: Search, sort, pagination and reactive vars

In this step we are going to add:

  • parties list pagination
  • sorting by party name
  • lastly, we will move our previously implemented parties location search to the server side.

Pagination simply means delivering and showing parties to the client on a page-by-page basis, where each page has a predefined number of items. Pagination reduces the number of documents to be transferred at one time thus decreasing load time. It also increases the usability of the user interface if there are too many documents in the storage.

Besides client-side logic, it usually includes querying a specific page of parties on the server side to deliver up to the client as well.

Pagination

First off, we'll add pagination on the server side.

Thanks to the simplicity of the Mongo API combined with Meteor's power, we only need to execute Parties.find on the server with some additional parameters. Keep in mind, with Meteor's isomorphic environment, we'll query Parties on the client with the same parameters as on the server.

Mongo Collection query options

Collection.find has a convenient second parameter called options, which takes an object for configuring collection querying. To implement pagination we'll need to provide sort, limit, and skip fields as options.

While limit and skip set boundaries on the result set, sort, at the same time, may not. We'll use sort to guarantee consistency of our pagination across page changes and page loads, since Mongo doesn't guarantee any order of documents if they are queried and not sorted. You can find more information about the find method in Mongo here.

Now, let's go to the parties subscription in the server/imports/publications/parties.ts file, add the options parameter to the subscription method, and then pass it to Parties.find:

Step 13.1: Add options to the parties publication

Changed server/imports/publications/parties.ts
@@ -1,8 +1,12 @@
 β”Š 1β”Š 1β”Šimport { Meteor } from 'meteor/meteor';
 β”Š 2β”Š 2β”Šimport { Parties } from '../../../both/collections/parties.collection';
 β”Š 3β”Š 3β”Š
-β”Š 4β”Š  β”ŠMeteor.publish('parties', function() {
-β”Š 5β”Š  β”Š  return Parties.find(buildQuery.call(this));
+β”Š  β”Š 4β”Šinterface Options {
+β”Š  β”Š 5β”Š  [key: string]: any;
+β”Š  β”Š 6β”Š}
+β”Š  β”Š 7β”Š
+β”Š  β”Š 8β”ŠMeteor.publish('parties', function(options: Options) {
+β”Š  β”Š 9β”Š  return Parties.find(buildQuery.call(this), options);
 β”Š 6β”Š10β”Š});
 β”Š 7β”Š11β”Š
 β”Š 8β”Š12β”ŠMeteor.publish('party', function(partyId: string) {

On the client side, we are going to define three additional variables in the PartiesList component which our pagination will depend on: page size, current page number and name sort order. Secondly, we'll create a special options object made up of these variables and pass it to the parties subscription:

Step 13.2: Define options and use it in the subscription

Changed client/imports/app/parties/parties-list.component.ts
@@ -8,6 +8,15 @@
 β”Š 8β”Š 8β”Š
 β”Š 9β”Š 9β”Šimport template from './parties-list.component.html';
 β”Š10β”Š10β”Š
+β”Š  β”Š11β”Šinterface Pagination {
+β”Š  β”Š12β”Š  limit: number;
+β”Š  β”Š13β”Š  skip: number;
+β”Š  β”Š14β”Š}
+β”Š  β”Š15β”Š
+β”Š  β”Š16β”Šinterface Options extends Pagination {
+β”Š  β”Š17β”Š  [key: string]: any
+β”Š  β”Š18β”Š}
+β”Š  β”Š19β”Š
 β”Š11β”Š20β”Š@Component({
 β”Š12β”Š21β”Š  selector: 'parties-list',
 β”Š13β”Š22β”Š  template
@@ -15,10 +24,20 @@
 β”Š15β”Š24β”Šexport class PartiesListComponent implements OnInit, OnDestroy {
 β”Š16β”Š25β”Š  parties: Observable<Party[]>;
 β”Š17β”Š26β”Š  partiesSub: Subscription;
+β”Š  β”Š27β”Š  pageSize: number = 10;
+β”Š  β”Š28β”Š  curPage: number = 1;
+β”Š  β”Š29β”Š  nameOrder: number = 1;
 β”Š18β”Š30β”Š
 β”Š19β”Š31β”Š  ngOnInit() {
-β”Š20β”Š  β”Š    this.parties = Parties.find({}).zone();
-β”Š21β”Š  β”Š    this.partiesSub = MeteorObservable.subscribe('parties').subscribe();
+β”Š  β”Š32β”Š    const options: Options = {
+β”Š  β”Š33β”Š      limit: this.pageSize,
+β”Š  β”Š34β”Š      skip: (this.curPage - 1) * this.pageSize,
+β”Š  β”Š35β”Š      sort: { name: this.nameOrder }
+β”Š  β”Š36β”Š    };
+β”Š  β”Š37β”Š
+β”Š  β”Š38β”Š    this.partiesSub = MeteorObservable.subscribe('parties', options).subscribe(() => {
+β”Š  β”Š39β”Š      this.parties = Parties.find({}).zone();
+β”Š  β”Š40β”Š    });
 β”Š22β”Š41β”Š  }
 β”Š23β”Š42β”Š
 β”Š24β”Š43β”Š  removeParty(party: Party): void {

As was said before, we also need to query Parties on the client side with same parameters and options as we used on the server, i.e., parameters and options we pass to the server side.

In reality, though, we don't need skip and limit options in this case, since the subscription result of the parties collection will always have a maximum page size of documents on the client.

So, we will only add sorting:

Step 13.3: Add sorting by party name to PartiesList

Changed client/imports/app/parties/parties-list.component.ts
@@ -36,7 +36,11 @@
 β”Š36β”Š36β”Š    };
 β”Š37β”Š37β”Š
 β”Š38β”Š38β”Š    this.partiesSub = MeteorObservable.subscribe('parties', options).subscribe(() => {
-β”Š39β”Š  β”Š      this.parties = Parties.find({}).zone();
+β”Š  β”Š39β”Š      this.parties = Parties.find({}, {
+β”Š  β”Š40β”Š        sort: {
+β”Š  β”Š41β”Š          name: this.nameOrder
+β”Š  β”Š42β”Š        }
+β”Š  β”Š43β”Š      }).zone();
 β”Š40β”Š44β”Š    });
 β”Š41β”Š45β”Š  }

Reactive Changes

The idea behind Reactive variables and changes - is to update our Meteor subscription according to the user interaction - for example: if the user changes the sort order - we want to drop the old Meteor subscription and replace it with a new one that matches the new parameters.

Because we are using RxJS, we can create variables that are Observables - which means we can register to the changes notification - and act as required - in our case - changed the Meteor subscription.

In order to do so, we will use RxJS Subject - which is an extension for Observable.

A Subject is a sort of bridge or proxy that is available in some implementations of RxJS that acts both as an observer and as an Observable.

Which means we can both register to the updates notifications and trigger the notification!

In our case - when the user changes the parameters of the Meteor subscription, we need to trigger the notification.

So let's do it. We will replace the regular variables with Subjects, and in order to trigger the notification in the first time, we will execute next() for the Subjects:

Step 13.4: Turn primitive values into Subjects

Changed client/imports/app/parties/parties-list.component.ts
@@ -1,5 +1,6 @@
 β”Š1β”Š1β”Šimport { Component, OnInit, OnDestroy } from '@angular/core';
 β”Š2β”Š2β”Šimport { Observable } from 'rxjs/Observable';
+β”Š β”Š3β”Šimport { Subject } from 'rxjs/Subject';
 β”Š3β”Š4β”Šimport { Subscription } from 'rxjs/Subscription';
 β”Š4β”Š5β”Šimport { MeteorObservable } from 'meteor-rxjs';
 β”Š5β”Š6β”Š
@@ -24,9 +25,9 @@
 β”Š24β”Š25β”Šexport class PartiesListComponent implements OnInit, OnDestroy {
 β”Š25β”Š26β”Š  parties: Observable<Party[]>;
 β”Š26β”Š27β”Š  partiesSub: Subscription;
-β”Š27β”Š  β”Š  pageSize: number = 10;
-β”Š28β”Š  β”Š  curPage: number = 1;
-β”Š29β”Š  β”Š  nameOrder: number = 1;
+β”Š  β”Š28β”Š  pageSize: Subject<number> = new Subject<number>();
+β”Š  β”Š29β”Š  curPage: Subject<number> = new Subject<number>();
+β”Š  β”Š30β”Š  nameOrder: Subject<number> = new Subject<number>();
 β”Š30β”Š31β”Š
 β”Š31β”Š32β”Š  ngOnInit() {
 β”Š32β”Š33β”Š    const options: Options = {
@@ -42,6 +43,10 @@
 β”Š42β”Š43β”Š        }
 β”Š43β”Š44β”Š      }).zone();
 β”Š44β”Š45β”Š    });
+β”Š  β”Š46β”Š
+β”Š  β”Š47β”Š    this.pageSize.next(10);
+β”Š  β”Š48β”Š    this.curPage.next(1);
+β”Š  β”Š49β”Š    this.nameOrder.next(1);
 β”Š45β”Š50β”Š  }
 β”Š46β”Š51β”Š
 β”Š47β”Š52β”Š  removeParty(party: Party): void {

Now we need to register to those changes notifications.

Because we need to register to multiple notifications (page size, current page, sort), we need to use a special RxJS Operator called combineLatest - which combines multiple Observables into one, and trigger a notification when one of them changes!

So let's use it and update the subscription:

Step 13.5: Re-subscribe on current page changes

Changed client/imports/app/parties/parties-list.component.ts
@@ -4,6 +4,8 @@
 β”Š 4β”Š 4β”Šimport { Subscription } from 'rxjs/Subscription';
 β”Š 5β”Š 5β”Šimport { MeteorObservable } from 'meteor-rxjs';
 β”Š 6β”Š 6β”Š
+β”Š  β”Š 7β”Šimport 'rxjs/add/operator/combineLatest';
+β”Š  β”Š 8β”Š
 β”Š 7β”Š 9β”Šimport { Parties } from '../../../../both/collections/parties.collection';
 β”Š 8β”Š10β”Šimport { Party } from '../../../../both/models/party.model';
 β”Š 9β”Š11β”Š
@@ -28,20 +30,31 @@
 β”Š28β”Š30β”Š  pageSize: Subject<number> = new Subject<number>();
 β”Š29β”Š31β”Š  curPage: Subject<number> = new Subject<number>();
 β”Š30β”Š32β”Š  nameOrder: Subject<number> = new Subject<number>();
+β”Š  β”Š33β”Š  optionsSub: Subscription;
 β”Š31β”Š34β”Š
 β”Š32β”Š35β”Š  ngOnInit() {
-β”Š33β”Š  β”Š    const options: Options = {
-β”Š34β”Š  β”Š      limit: this.pageSize,
-β”Š35β”Š  β”Š      skip: (this.curPage - 1) * this.pageSize,
-β”Š36β”Š  β”Š      sort: { name: this.nameOrder }
-β”Š37β”Š  β”Š    };
-β”Š38β”Š  β”Š
-β”Š39β”Š  β”Š    this.partiesSub = MeteorObservable.subscribe('parties', options).subscribe(() => {
-β”Š40β”Š  β”Š      this.parties = Parties.find({}, {
-β”Š41β”Š  β”Š        sort: {
-β”Š42β”Š  β”Š          name: this.nameOrder
-β”Š43β”Š  β”Š        }
-β”Š44β”Š  β”Š      }).zone();
+β”Š  β”Š36β”Š    this.optionsSub = Observable.combineLatest(
+β”Š  β”Š37β”Š      this.pageSize,
+β”Š  β”Š38β”Š      this.curPage,
+β”Š  β”Š39β”Š      this.nameOrder
+β”Š  β”Š40β”Š    ).subscribe(([pageSize, curPage, nameOrder]) => {
+β”Š  β”Š41β”Š      const options: Options = {
+β”Š  β”Š42β”Š        limit: pageSize as number,
+β”Š  β”Š43β”Š        skip: ((curPage as number) - 1) * (pageSize as number),
+β”Š  β”Š44β”Š        sort: { name: nameOrder as number }
+β”Š  β”Š45β”Š      };
+β”Š  β”Š46β”Š
+β”Š  β”Š47β”Š      if (this.partiesSub) {
+β”Š  β”Š48β”Š        this.partiesSub.unsubscribe();
+β”Š  β”Š49β”Š      }
+β”Š  β”Š50β”Š      
+β”Š  β”Š51β”Š      this.partiesSub = MeteorObservable.subscribe('parties', options).subscribe(() => {
+β”Š  β”Š52β”Š        this.parties = Parties.find({}, {
+β”Š  β”Š53β”Š          sort: {
+β”Š  β”Š54β”Š            name: nameOrder
+β”Š  β”Š55β”Š          }
+β”Š  β”Š56β”Š        }).zone();
+β”Š  β”Š57β”Š      });
 β”Š45β”Š58β”Š    });
 β”Š46β”Š59β”Š
 β”Š47β”Š60β”Š    this.pageSize.next(10);
@@ -59,5 +72,6 @@
 β”Š59β”Š72β”Š
 β”Š60β”Š73β”Š  ngOnDestroy() {
 β”Š61β”Š74β”Š    this.partiesSub.unsubscribe();
+β”Š  β”Š75β”Š    this.optionsSub.unsubscribe();
 β”Š62β”Š76β”Š  }
 β”Š63β”Š77β”Š}

Notice that we also removes the Subscription and use unsubscribe because we want to drop the old subscription each time it changes.

Pagination UI

As this paragraph name suggests, the next logical thing to do would be to implement a pagination UI, which consists of, at least, a list of page links at the bottom of every page, so that the user can switch pages by clicking on these links.

Creating a pagination component is not a trivial task and not one of the primary goals of this tutorial, so we are going to make use of an already existing package with Angular 2 pagination components. Run the following line to add this package:

$ meteor npm install ng2-pagination --save

This package's pagination mark-up follows the structure of the Bootstrap pagination component, so you can change its look simply by using proper CSS styles. It's worth noting, though, that this package has been created with the only this tutorial in mind. It misses a lot of features that would be quite useful in the real world, for example, custom templates.

Ng2-Pagination consists of three main parts:

  • pagination controls that render a list of links
  • a pagination service to manipulate logic programmatically
  • a pagination pipe component, which can be added in any component template, with the main goal to transform a list of items according to the current state of the pagination service and show current page of items on UI

First, let's import the pagination module into our NgModule:

Step 13.7: Import Ng2PaginationModule

Changed client/imports/app/app.module.ts
@@ -3,6 +3,7 @@
 β”Š3β”Š3β”Šimport { FormsModule, ReactiveFormsModule } from '@angular/forms';
 β”Š4β”Š4β”Šimport { RouterModule } from '@angular/router';
 β”Š5β”Š5β”Šimport { AccountsModule } from 'angular2-meteor-accounts-ui';
+β”Š β”Š6β”Šimport { Ng2PaginationModule } from 'ng2-pagination';
 β”Š6β”Š7β”Š
 β”Š7β”Š8β”Šimport { AppComponent } from './app.component';
 β”Š8β”Š9β”Šimport { routes, ROUTES_PROVIDERS } from './app.routes';
@@ -14,7 +15,8 @@
 β”Š14β”Š15β”Š    FormsModule,
 β”Š15β”Š16β”Š    ReactiveFormsModule,
 β”Š16β”Š17β”Š    RouterModule.forRoot(routes),
-β”Š17β”Š  β”Š    AccountsModule
+β”Š  β”Š18β”Š    AccountsModule,
+β”Š  β”Š19β”Š    Ng2PaginationModule
 β”Š18β”Š20β”Š  ],
 β”Š19β”Š21β”Š  declarations: [
 β”Š20β”Š22β”Š    AppComponent,

Because of pagination pipe of ng2-pagination supports only arrays we'll use the PaginationService. Let's define the configuration:

Step 13.8: Register configuration of pagination

Changed client/imports/app/parties/parties-list.component.ts
@@ -3,6 +3,7 @@
 β”Š3β”Š3β”Šimport { Subject } from 'rxjs/Subject';
 β”Š4β”Š4β”Šimport { Subscription } from 'rxjs/Subscription';
 β”Š5β”Š5β”Šimport { MeteorObservable } from 'meteor-rxjs';
+β”Š β”Š6β”Šimport { PaginationService } from 'ng2-pagination';
 β”Š6β”Š7β”Š
 β”Š7β”Š8β”Šimport 'rxjs/add/operator/combineLatest';
 β”Š8β”Š9β”Š
@@ -32,6 +33,10 @@
 β”Š32β”Š33β”Š  nameOrder: Subject<number> = new Subject<number>();
 β”Š33β”Š34β”Š  optionsSub: Subscription;
 β”Š34β”Š35β”Š
+β”Š  β”Š36β”Š  constructor(
+β”Š  β”Š37β”Š    private paginationService: PaginationService
+β”Š  β”Š38β”Š  ) {}
+β”Š  β”Š39β”Š
 β”Š35β”Š40β”Š  ngOnInit() {
 β”Š36β”Š41β”Š    this.optionsSub = Observable.combineLatest(
 β”Š37β”Š42β”Š      this.pageSize,
@@ -57,6 +62,13 @@
 β”Š57β”Š62β”Š      });
 β”Š58β”Š63β”Š    });
 β”Š59β”Š64β”Š
+β”Š  β”Š65β”Š    this.paginationService.register({
+β”Š  β”Š66β”Š      id: this.paginationService.defaultId,
+β”Š  β”Š67β”Š      itemsPerPage: 10,
+β”Š  β”Š68β”Š      currentPage: 1,
+β”Š  β”Š69β”Š      totalItems: 30,
+β”Š  β”Š70β”Š    });
+β”Š  β”Š71β”Š
 β”Š60β”Š72β”Š    this.pageSize.next(10);
 β”Š61β”Š73β”Š    this.curPage.next(1);
 β”Š62β”Š74β”Š    this.nameOrder.next(1);

id - this is the identifier of specific pagination, we use the default.

We need to notify the pagination that the current page has been changed, so let's add it to the method where we handle the reactive changes:

Step 13.9: Update current page when options change

Changed client/imports/app/parties/parties-list.component.ts
@@ -49,6 +49,8 @@
 β”Š49β”Š49β”Š        sort: { name: nameOrder as number }
 β”Š50β”Š50β”Š      };
 β”Š51β”Š51β”Š
+β”Š  β”Š52β”Š      this.paginationService.setCurrentPage(this.paginationService.defaultId, curPage as number);
+β”Š  β”Š53β”Š
 β”Š52β”Š54β”Š      if (this.partiesSub) {
 β”Š53β”Š55β”Š        this.partiesSub.unsubscribe();
 β”Š54β”Š56β”Š      }

Now, add the pagination controls to the parties-list.component.html template:

Step 13.10: Add pagination to the list

Changed client/imports/app/parties/parties-list.component.html
@@ -13,4 +13,6 @@
 β”Š13β”Š13β”Š      <button (click)="removeParty(party)">X</button>
 β”Š14β”Š14β”Š    </li>
 β”Š15β”Š15β”Š  </ul>
+β”Š  β”Š16β”Š
+β”Š  β”Š17β”Š  <pagination-controls></pagination-controls>
 β”Š16β”Š18β”Š</div>πŸš«β†΅

In the configuration, we provided the current page number, the page size and a new value of total items in the list to paginate.

This total number of items are required to be set in our case, since we don't provide a regular array of elements but instead an Observable, the pagination service simply won't know how to calculate its size.

We'll get back to this in the next paragraph where we'll be setting parties total size reactively.

For now, let's just set it to be 30. We'll see why this default value is needed shortly.

pageChange events

The final part is to handle user clicks on the page links. The pagination controls component fires a special event when the user clicks on a page link, causing the current page to update.

Let's handle this event in the template first and then add a method to the PartiesList component itself:

Step 13.11: Add pageChange event binding

Changed client/imports/app/parties/parties-list.component.html
@@ -14,5 +14,5 @@
 β”Š14β”Š14β”Š    </li>
 β”Š15β”Š15β”Š  </ul>
 β”Š16β”Š16β”Š
-β”Š17β”Š  β”Š  <pagination-controls></pagination-controls>
+β”Š  β”Š17β”Š  <pagination-controls (pageChange)="onPageChanged($event)"></pagination-controls>
 β”Š18β”Š18β”Š</div>πŸš«β†΅

As you can see, the pagination controls component fires the pageChange event, calling the onPageChanged method with a special event object that contains the new page number to set. Add the onPageChanged method:

Step 13.12: Add event handler for pageChange

Changed client/imports/app/parties/parties-list.component.ts
@@ -84,6 +84,10 @@
 β”Š84β”Š84β”Š    this.parties = Parties.find(value ? { location: value } : {}).zone();
 β”Š85β”Š85β”Š  }
 β”Š86β”Š86β”Š
+β”Š  β”Š87β”Š  onPageChanged(page: number): void {
+β”Š  β”Š88β”Š    this.curPage.next(page);
+β”Š  β”Š89β”Š  }
+β”Š  β”Š90β”Š
 β”Š87β”Š91β”Š  ngOnDestroy() {
 β”Š88β”Š92β”Š    this.partiesSub.unsubscribe();
 β”Š89β”Š93β”Š    this.optionsSub.unsubscribe();

At this moment, we have almost everything in place. Let's check if everything is working. We are going to have to add a lot of parties, at least, a couple of pages. But, since we've chosen quite a large default page size (10), it would be tedious to add all parties manually.

Generating Mock Data

In this example, we need to deal with multiple objects and in order to test it and get the best results - we need a lot of Parties objects.

Thankfully, we have a helpful package called anti:fake, which will help us out with the generation of names, locations and other properties of new fake parties.

$ meteor add anti:fake

So, with the following lines of code we are going to have 30 parties in total (given that we already have three):

server/imports/fixtures/parties.ts:

...

for (var i = 0; i < 27; i++) {
  Parties.insert({
    name: Fake.sentence(50),
    location: Fake.sentence(10),
    description: Fake.sentence(100),
    public: true
  });
}

Fake is loaded in Meteor as a global, you may want to declare it for TypeScript.

You can add it to the end of the typings.d.ts file:

declare var Fake: {
    sentence(words: number): string;
}

Now reset the database (meteor reset) and run the app. You should see a list of 10 parties shown initially and 3 pages links just at the bottom.

Play around with the pagination: click on page links to go back and forth, then try to delete parties to check if the current page updates properly.

Getting the Total Number of Parties

The pagination component needs to know how many pages it will create. As such, we need to know the total number of parties in storage and divide it by the number of items per page.

At the same time, our parties collection will always have no more than necessary parties on the client side. This suggests that we have to add a new publication to publish only the current count of parties existing in storage.

This task looks quite common and, thankfully, it's already been implemented. We can use the tmeasday:publish-counts package.

$ meteor add tmeasday:publish-counts

This package is an example for a package that does not provide it's own TypeScript declaration file, so we will have to manually create and add it to the typings.d.ts file according to the package API:

Step 13.15: Add declaration of counts package

Changed typings.d.ts
@@ -25,4 +25,15 @@
 β”Š25β”Š25β”Šdeclare module '*.sass' {
 β”Š26β”Š26β”Š  const style: string;
 β”Š27β”Š27β”Š  export default style;
-β”Š28β”Š  β”Š}πŸš«β†΅
+β”Š  β”Š28β”Š}
+β”Š  β”Š29β”Š
+β”Š  β”Š30β”Šdeclare module 'meteor/tmeasday:publish-counts' {
+β”Š  β”Š31β”Š  import { Mongo } from 'meteor/mongo';
+β”Š  β”Š32β”Š
+β”Š  β”Š33β”Š  interface CountsObject {
+β”Š  β”Š34β”Š    get(publicationName: string): number;
+β”Š  β”Š35β”Š    publish(context: any, publicationName: string, cursor: Mongo.Cursor, options: any): number;
+β”Š  β”Š36β”Š  }
+β”Š  β”Š37β”Š
+β”Š  β”Š38β”Š  export const Counts: CountsObject;
+β”Š  β”Š39β”Š}

This package exports a Counts object with all of the API methods we will need.

Notice that you'll see a TypeScript warning in the terminal saying that "Counts" has no method you want to use, when you start using the API. You can remove this warning by adding a publish-counts type declaration file to your typings.

Let's publish the total number of parties as follows:

Step 13.14: Publish total number of parties

Changed server/imports/publications/parties.ts
@@ -1,4 +1,6 @@
 β”Š1β”Š1β”Šimport { Meteor } from 'meteor/meteor';
+β”Š β”Š2β”Šimport { Counts } from 'meteor/tmeasday:publish-counts';
+β”Š β”Š3β”Š
 β”Š2β”Š4β”Šimport { Parties } from '../../../both/collections/parties.collection';
 β”Š3β”Š5β”Š
 β”Š4β”Š6β”Šinterface Options {
@@ -6,6 +8,8 @@
 β”Š 6β”Š 8β”Š}
 β”Š 7β”Š 9β”Š
 β”Š 8β”Š10β”ŠMeteor.publish('parties', function(options: Options) {
+β”Š  β”Š11β”Š  Counts.publish(this, 'numberOfParties', Parties.collection.find(buildQuery.call(this)), { noReady: true });
+β”Š  β”Š12β”Š
 β”Š 9β”Š13β”Š  return Parties.find(buildQuery.call(this), options);
 β”Š10β”Š14β”Š});

Notice that we are passing { noReady: true } in the last argument so that the publication will be ready only after our main cursor is loaded, instead of waiting for Counts.

We've just created the new numberOfParties publication. Let's get it reactively on the client side using the Counts object, and, at the same time, introduce a new partiesSize property in the PartiesList component:

Step 13.16: Handle reactive updates of the parties total number

Changed client/imports/app/parties/parties-list.component.ts
@@ -4,6 +4,7 @@
 β”Š 4β”Š 4β”Šimport { Subscription } from 'rxjs/Subscription';
 β”Š 5β”Š 5β”Šimport { MeteorObservable } from 'meteor-rxjs';
 β”Š 6β”Š 6β”Šimport { PaginationService } from 'ng2-pagination';
+β”Š  β”Š 7β”Šimport { Counts } from 'meteor/tmeasday:publish-counts';
 β”Š 7β”Š 8β”Š
 β”Š 8β”Š 9β”Šimport 'rxjs/add/operator/combineLatest';
 β”Š 9β”Š10β”Š
@@ -32,6 +33,8 @@
 β”Š32β”Š33β”Š  curPage: Subject<number> = new Subject<number>();
 β”Š33β”Š34β”Š  nameOrder: Subject<number> = new Subject<number>();
 β”Š34β”Š35β”Š  optionsSub: Subscription;
+β”Š  β”Š36β”Š  partiesSize: number = 0;
+β”Š  β”Š37β”Š  autorunSub: Subscription;
 β”Š35β”Š38β”Š
 β”Š36β”Š39β”Š  constructor(
 β”Š37β”Š40β”Š    private paginationService: PaginationService
@@ -68,12 +71,17 @@
 β”Š68β”Š71β”Š      id: this.paginationService.defaultId,
 β”Š69β”Š72β”Š      itemsPerPage: 10,
 β”Š70β”Š73β”Š      currentPage: 1,
-β”Š71β”Š  β”Š      totalItems: 30,
+β”Š  β”Š74β”Š      totalItems: this.partiesSize
 β”Š72β”Š75β”Š    });
 β”Š73β”Š76β”Š
 β”Š74β”Š77β”Š    this.pageSize.next(10);
 β”Š75β”Š78β”Š    this.curPage.next(1);
 β”Š76β”Š79β”Š    this.nameOrder.next(1);
+β”Š  β”Š80β”Š
+β”Š  β”Š81β”Š    this.autorunSub = MeteorObservable.autorun().subscribe(() => {
+β”Š  β”Š82β”Š      this.partiesSize = Counts.get('numberOfParties');
+β”Š  β”Š83β”Š      this.paginationService.setTotalItems(this.paginationService.defaultId, this.partiesSize);
+β”Š  β”Š84β”Š    });
 β”Š77β”Š85β”Š  }
 β”Š78β”Š86β”Š
 β”Š79β”Š87β”Š  removeParty(party: Party): void {
@@ -91,5 +99,6 @@
 β”Š 91β”Š 99β”Š  ngOnDestroy() {
 β”Š 92β”Š100β”Š    this.partiesSub.unsubscribe();
 β”Š 93β”Š101β”Š    this.optionsSub.unsubscribe();
+β”Š   β”Š102β”Š    this.autorunSub.unsubscribe();
 β”Š 94β”Š103β”Š  }
 β”Š 95β”Š104β”Š}

We used MeteorObservable.autorun because we wan't to know when there are changes regarding the data that comes from Meteor - so now every change of data, we will calculate the total number of parties and save it in our Component, then we will set it in the PaginationService.

Let's verify that the app works the same as before. Run the app. There should be same three pages of parties.

What's more interesting is to add a couple of new parties, thus, adding a new 4th page. By this way, we can prove that our new "total number" publication and pagination controls are all working properly.

Changing Sort Order

It's time for a new cool feature Socially users will certainly enjoy - sorting the parties list by party name. At this moment, we know everything we need to implement it.

As previously implements, nameOrder uses one of two values, 1 or -1, to express ascending and descending orders respectively. Then, as you can see, we assign nameOrder to the party property (currently, name) we want to sort.

We'll add a new dropdown UI control with two orders to change, ascending and descending. Let's add it in front of our parties list:

Step 13.17: Add the sort order dropdown

Changed client/imports/app/parties/parties-list.component.html
@@ -5,6 +5,15 @@
 β”Š 5β”Š 5β”Š  
 β”Š 6β”Š 6β”Š  <login-buttons></login-buttons>
 β”Š 7β”Š 7β”Š
+β”Š  β”Š 8β”Š  <h1>Parties:</h1>
+β”Š  β”Š 9β”Š
+β”Š  β”Š10β”Š  <div>
+β”Š  β”Š11β”Š    <select #sort (change)="changeSortOrder(sort.value)">
+β”Š  β”Š12β”Š      <option value="1" selected>Ascending</option>
+β”Š  β”Š13β”Š      <option value="-1">Descending</option>
+β”Š  β”Š14β”Š    </select>
+β”Š  β”Š15β”Š  </div>
+β”Š  β”Š16β”Š
 β”Š 8β”Š17β”Š  <ul>
 β”Š 9β”Š18β”Š    <li *ngFor="let party of parties | async">
 β”Š10β”Š19β”Š      <a [routerLink]="['/party', party._id]">{{party.name}}</a>

In the PartiesList component, we change the nameOrder property to be a reactive variable and add a changeSortOrder event handler, where we set the new sort order:

Step 13.18: Re-subscribe when sort order changes

Changed client/imports/app/parties/parties-list.component.ts
@@ -96,6 +96,10 @@
 β”Š 96β”Š 96β”Š    this.curPage.next(page);
 β”Š 97β”Š 97β”Š  }
 β”Š 98β”Š 98β”Š
+β”Š   β”Š 99β”Š  changeSortOrder(nameOrder: string): void {
+β”Š   β”Š100β”Š    this.nameOrder.next(parseInt(nameOrder));
+β”Š   β”Š101β”Š  }
+β”Š   β”Š102β”Š
 β”Š 99β”Š103β”Š  ngOnDestroy() {
 β”Š100β”Š104β”Š    this.partiesSub.unsubscribe();
 β”Š101β”Š105β”Š    this.optionsSub.unsubscribe();

Calling next on nameOrder Subject, will trigger the change notification - and then the Meteor subscription will re-created with the new parameters!

That's just it! Run the app and change the sort order back and forth.

What's important here is that pagination updates properly, i.e. according to a new sort order.

Server Side Search

Before this step we had a nice feature to search parties by location, but with the addition of pagination, location search has partly broken. In its current state, there will always be no more than the current page of parties shown simultaneously on the client side. We would like, of course, to search parties across all storage, not just across the current page.

To fix that, we'll need to patch our "parties" and "total number" publications on the server side to query parties with a new "location" parameter passed down from the client. Having that fixed, it should work properly in accordance with the added pagination.

So, let's add filtering parties by the location with the help of Mongo's regex API. It is going to look like this:

Step 13.19: Add search by the party location using regex

Changed server/imports/publications/parties.ts
@@ -7,10 +7,12 @@
 β”Š 7β”Š 7β”Š  [key: string]: any;
 β”Š 8β”Š 8β”Š}
 β”Š 9β”Š 9β”Š
-β”Š10β”Š  β”ŠMeteor.publish('parties', function(options: Options) {
-β”Š11β”Š  β”Š  Counts.publish(this, 'numberOfParties', Parties.collection.find(buildQuery.call(this)), { noReady: true });
+β”Š  β”Š10β”ŠMeteor.publish('parties', function(options: Options, location?: string) {
+β”Š  β”Š11β”Š  const selector = buildQuery.call(this, null, location);
 β”Š12β”Š12β”Š
-β”Š13β”Š  β”Š  return Parties.find(buildQuery.call(this), options);
+β”Š  β”Š13β”Š  Counts.publish(this, 'numberOfParties', Parties.collection.find(selector), { noReady: true });
+β”Š  β”Š14β”Š
+β”Š  β”Š15β”Š  return Parties.find(selector, options);
 β”Š14β”Š16β”Š});
 β”Š15β”Š17β”Š
 β”Š16β”Š18β”ŠMeteor.publish('party', function(partyId: string) {
@@ -18,7 +20,7 @@
 β”Š18β”Š20β”Š});
 β”Š19β”Š21β”Š
 β”Š20β”Š22β”Š
-β”Š21β”Š  β”Šfunction buildQuery(partyId?: string): Object {
+β”Š  β”Š23β”Šfunction buildQuery(partyId?: string, location?: string): Object {
 β”Š22β”Š24β”Š  const isAvailable = {
 β”Š23β”Š25β”Š    $or: [{
 β”Š24β”Š26β”Š      // party is public
@@ -48,5 +50,13 @@
 β”Š48β”Š50β”Š    };
 β”Š49β”Š51β”Š  }
 β”Š50β”Š52β”Š
-β”Š51β”Š  β”Š  return isAvailable;
+β”Š  β”Š53β”Š  const searchRegEx = { '$regex': '.*' + (location || '') + '.*', '$options': 'i' };
+β”Š  β”Š54β”Š
+β”Š  β”Š55β”Š  return {
+β”Š  β”Š56β”Š    $and: [{
+β”Š  β”Š57β”Š        location: searchRegEx
+β”Š  β”Š58β”Š      },
+β”Š  β”Š59β”Š      isAvailable
+β”Š  β”Š60β”Š    ]
+β”Š  β”Š61β”Š  };
 β”Š52β”Š62β”Š}πŸš«β†΅

On the client side, we are going to add a new reactive variable and set it to update when a user clicks on the search button:

Step 13.20: Add reactive search by location

Changed client/imports/app/parties/parties-list.component.ts
@@ -35,6 +35,7 @@
 β”Š35β”Š35β”Š  optionsSub: Subscription;
 β”Š36β”Š36β”Š  partiesSize: number = 0;
 β”Š37β”Š37β”Š  autorunSub: Subscription;
+β”Š  β”Š38β”Š  location: Subject<string> = new Subject<string>();
 β”Š38β”Š39β”Š
 β”Š39β”Š40β”Š  constructor(
 β”Š40β”Š41β”Š    private paginationService: PaginationService
@@ -44,8 +45,9 @@
 β”Š44β”Š45β”Š    this.optionsSub = Observable.combineLatest(
 β”Š45β”Š46β”Š      this.pageSize,
 β”Š46β”Š47β”Š      this.curPage,
-β”Š47β”Š  β”Š      this.nameOrder
-β”Š48β”Š  β”Š    ).subscribe(([pageSize, curPage, nameOrder]) => {
+β”Š  β”Š48β”Š      this.nameOrder,
+β”Š  β”Š49β”Š      this.location
+β”Š  β”Š50β”Š    ).subscribe(([pageSize, curPage, nameOrder, location]) => {
 β”Š49β”Š51β”Š      const options: Options = {
 β”Š50β”Š52β”Š        limit: pageSize as number,
 β”Š51β”Š53β”Š        skip: ((curPage as number) - 1) * (pageSize as number),
@@ -58,7 +60,7 @@
 β”Š58β”Š60β”Š        this.partiesSub.unsubscribe();
 β”Š59β”Š61β”Š      }
 β”Š60β”Š62β”Š      
-β”Š61β”Š  β”Š      this.partiesSub = MeteorObservable.subscribe('parties', options).subscribe(() => {
+β”Š  β”Š63β”Š      this.partiesSub = MeteorObservable.subscribe('parties', options, location).subscribe(() => {
 β”Š62β”Š64β”Š        this.parties = Parties.find({}, {
 β”Š63β”Š65β”Š          sort: {
 β”Š64β”Š66β”Š            name: nameOrder
@@ -77,6 +79,7 @@
 β”Š77β”Š79β”Š    this.pageSize.next(10);
 β”Š78β”Š80β”Š    this.curPage.next(1);
 β”Š79β”Š81β”Š    this.nameOrder.next(1);
+β”Š  β”Š82β”Š    this.location.next('');
 β”Š80β”Š83β”Š
 β”Š81β”Š84β”Š    this.autorunSub = MeteorObservable.autorun().subscribe(() => {
 β”Š82β”Š85β”Š      this.partiesSize = Counts.get('numberOfParties');
@@ -89,7 +92,8 @@
 β”Š89β”Š92β”Š  }
 β”Š90β”Š93β”Š
 β”Š91β”Š94β”Š  search(value: string): void {
-β”Š92β”Š  β”Š    this.parties = Parties.find(value ? { location: value } : {}).zone();
+β”Š  β”Š95β”Š    this.curPage.next(1);
+β”Š  β”Š96β”Š    this.location.next(value);
 β”Š93β”Š97β”Š  }
 β”Š94β”Š98β”Š
 β”Š95β”Š99β”Š  onPageChanged(page: number): void {

Notice that we don't know what size to expect from the search that's why we are re-setting the current page to 1.

Let's check it out now that everything works properly altogether: pagination, search, sorting, removing and addition of new parties.

For example, you can try to add 30 parties in a way mentioned slightly above; then try to remove all 30 parties; then sort by the descending order; then try to search by Palo Alto β€” it should find only two, in case if you have not added any other parties rather than used in this tutorial so far; then try to remove one of the found parties and, finally, search with an empty location.

Although this sequence of actions looks quite complicated, it was accomplished with rather few lines of code.

Summary

This step covered a lot. We looked at:

  • Mongo query sort options: sort, limit, skip
  • RxJS Subject for updating variables automatically
  • Handling onChange events in Angular 2
  • Generating fake data with anti:fake
  • Establishing the total number of results with tmeasday:publish-counts
  • Enabling server-side searching across an entire collection

In the next step we'll look at sending out our party invitations and look deeper into pipes.

}: # {: (footer) {: (nav_step)

< Previous Step Next Step >
}: #
}: #