AngularJS Pipes

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

In this and next steps we are going to:

  • add party invitations;
  • filter data with Angular2 pipes
  • learn about Meteor methods

Rendering Users

We'll start by working on the PartyDetails component. Each party owner should be able to invite multiple guests to a party, hence, the user needs to be able to manipulate the data on the party details page.

First of all, we'll need to render a list of all users to invite on the page. Since we've made the app secure during step 8 by removing the insecure package, to get a list of users — the same as for the parties — we'll need to create a new publication, and then subscribe to load the user collection.

Let's create a new file server/imports/publications/users.ts and add a new publication there. We can start by finding all uninvited users, specifically, users who are not invited and not the current user.

14.1 Add uninvited users publication server/imports/publications/users.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Meteor } from 'meteor/meteor';
 
import { Parties } from '../../../both/collections/parties.collection';
 
Meteor.publish('uninvited', function (partyId: string) {
  const party = Parties.findOne(partyId);
 
  if (!party) {
    throw new Meteor.Error('404', 'No such party!');
  }
 
  return Meteor.users.find({
    _id: {
      $nin: party.invited || [],
      $ne: this.userId
    }
  });
});

Notice that we've made use of a special Mongo selector $nin, meaning "not in", to sift out users that have already been invited to this party so far. We used $ne to select ids that are "not equal" to the user's id.

As you can see above, we've introduced a new party property — "invited", which is going to be an array of all invited user IDs.

Now, let's update the Party interface to contain the new field:

14.2 Add invited to Party interface both/models/party.model.ts
6
7
8
9
10
  location: string;
  owner?: string;
  public: boolean;
  invited?: string[];
}

Next, import the users publication to be defined on the server during startup:

14.3 Use it on the server server/main.ts
2
3
4
5
6
7
8
9
 
import { loadParties } from './imports/fixtures/parties';
 
import './imports/publications/parties';
import './imports/publications/users'; 
 
Meteor.startup(() => {
  loadParties();

Now, let's create a new Collection with RxJS support for the users collection. Meteor have a built-in users collection, so we just need to wrap it using MongoObservable.fromExisting:

14.4 Create Users collection from Meteor.users both/collections/users.collection.ts
1
2
3
4
import { MongoObservable } from 'meteor-rxjs';
import { Meteor } from 'meteor/meteor';
 
export const Users = MongoObservable.fromExisting(Meteor.users);

And let's create an interface for the User object:

14.5 Create also User interface both/models/user.model.ts
1
2
3
import { Meteor } from 'meteor/meteor';
 
export interface User extends Meteor.User {}

Then, let's load the uninvited users of a particular party into the PartyDetails component.

We will use MeteorObservable.subscribe to subscribe to the data, and we use .find on the Users collection in order to fetch the user's details:

14.6 Implement subscription of uninvited users client/imports/app/parties/party-details.component.ts
1
2
3
4
5
6
 
9
10
11
12
13
14
15
16
 
23
24
25
26
27
28
29
30
 
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
 
76
77
78
79
80
81
import { Component, OnInit, OnDestroy } from [email protected]/core';
import { ActivatedRoute } from [email protected]/router';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Meteor } from 'meteor/meteor';
import { MeteorObservable } from 'meteor-rxjs';
...some lines skipped...
 
import { Parties } from '../../../../both/collections/parties.collection';
import { Party } from '../../../../both/models/party.model';
import { Users } from '../../../../both/collections/users.collection';
import { User } from '../../../../both/models/user.model';
 
import template from './party-details.component.html';
 
...some lines skipped...
  paramsSub: Subscription;
  party: Party;
  partySub: Subscription;
  users: Observable<User>;
  uninvitedSub: Subscription;
 
  constructor(
    private route: ActivatedRoute
...some lines skipped...
        this.partySub = MeteorObservable.subscribe('party', this.partyId).subscribe(() => {
          this.party = Parties.findOne(this.partyId);
        });
 
        if (this.uninvitedSub) {
          this.uninvitedSub.unsubscribe();
        }
 
        this.uninvitedSub = MeteorObservable.subscribe('uninvited', this.partyId).subscribe(() => {
           this.users = Users.find({
             _id: {
               $ne: Meteor.userId()
              }
            }).zone();
        });
      });
  }
 
...some lines skipped...
  ngOnDestroy() {
    this.paramsSub.unsubscribe();
    this.partySub.unsubscribe();
    this.uninvitedSub.unsubscribe();
  }
}

Now, render the uninvited users on the PartyDetails's page:

14.7 Display list of uninvited users client/imports/app/parties/party-details.component.html
10
11
12
13
14
15
16
17
18
19
20
 
  <button type="submit">Save</button>
  <a [routerLink]="['/']">Cancel</a>
</form>
 
<p>Users to invite:</p>
<ul>
  <li *ngFor="let user of users | async">
    <div>{{user.emails[0].address}}</div>
  </li>
</ul>

Remember? we use async Pipe because we use RxJS Observable

Implementing Pipes

In the previous section we rendered a list of user emails. In Meteor's accounts package an email is considered a primary user ID by default. At the same time, everything is configurable, which means there is way for a user to set a custom username to be identified with during the registration. Considering usernames are more visually appealing than emails, let's render them instead of emails in that list of uninvited users checking if the name is set for each user.

For that purpose we could create a private component method and call it each time in the template to get the right display name, i.e., username or email. Instead, we'll implement a special pipe that handles this, at the same time, we'll learn how to create stateless pipes. One of the advantages of this approach in comparison to class methods is that we can use the same pipe in any component. You can read the docs to know more about Angular2 Pipes.

Let's add a new folder "client/imports/app/shared" and place a new file display-name.pipe.ts. We'll add our new displayName pipe inside of it:

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
import { Pipe, PipeTransform } from [email protected]/core';
import { Meteor } from 'meteor/meteor';
 
import { User } from '../../../../both/models/user.model';
 
@Pipe({
  name: 'displayName'
})
export class DisplayNamePipe implements PipeTransform {
  transform(user: User): string {
    if (!user) {
      return '';
    }
 
    if (user.username) {
      return user.username;
    }
 
    if (user.emails) {
      return user.emails[0].address;
    }
 
    return '';
  }
}

As you can see, there are a couple of things to remember in order to create a pipe:

  • define a class that implements the PipeTransform interface, with an implemented method transform inside
  • place pipe metadata upon this class with the help of the @Pipe decorator to notify Angular 2 that this class essentially is a pipe

Now, in order to use this Pipe, we need to declare it in the NgModule, so first let's create an index file for all of the shared declarations:

14.9 Make space for shared declarations client/imports/app/shared/index.ts
1
2
3
4
5
import { DisplayNamePipe } from './display-name.pipe';
 
export const SHARED_DECLARATIONS: any[] = [
  DisplayNamePipe
];

And import the exposed Array in our NgModule definition:

14.10 Import those declarations to AppModule client/imports/app/app.module.ts
8
9
10
11
12
13
14
 
21
22
23
24
25
26
27
28
import { AppComponent } from './app.component';
import { routes, ROUTES_PROVIDERS } from './app.routes';
import { PARTIES_DECLARATIONS } from './parties';
import { SHARED_DECLARATIONS } from './shared';
 
@NgModule({
  imports: [
...some lines skipped...
  ],
  declarations: [
    AppComponent,
    ...PARTIES_DECLARATIONS,
    ...SHARED_DECLARATIONS
  ],
  providers: [
    ...ROUTES_PROVIDERS

To make use of the created pipe, change the markup of the PartyDetails's template to:

14.11 Use this Pipe in PartyDetails client/imports/app/parties/party-details.component.html
15
16
17
18
19
20
<p>Users to invite:</p>
<ul>
  <li *ngFor="let user of users | async">
    <div>{{user | displayName}}</div>
  </li>
</ul>

If you were familiar with Angular 1's filters concept, you might believe that Angular 2's pipes are very similar. This is both true and not. While the view syntax and aims they are used for are the same, there are some important differences. The main one is that Angular 2 can now efficiently handle stateful pipes, whereas stateful filters were discouraged in Angular 1. Another thing to note is that Angular 2 pipes are defined in the unique and elegant Angular 2 way, i.e., using classes and class metadata, the same as for components and their views.

Challenge

In order to cement our knowledge of using pipes, try to create a current user status pipe, which transforms the current user in a party to one of three values: owner, invited and uninvited. This will be helpful in the next step, where we'll get to the implementation of the invitation feature and will change the current UI for improved security.

Summary

In this step, we learned about:

  • how to implement pipes in Angular 2, and how they different from filters in Angular 1
  • configuring our accounts-ui package
  • some Mongo query operators like $nin & $ne

In the next step we'll look at using Meteor methods as a way to securely verify client-side data changes on the server.