In this and next steps we are going to:
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.
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:
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:
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
:
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:
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:
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 '@angular/core';
import { ActivatedRoute } from '@angular/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:
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 RxJSObservable
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 '@angular/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:
PipeTransform
interface, with an implemented method transform
inside@Pipe
decorator to notify Angular 2 that this class essentially is a pipeNow, 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:
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:
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:
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.
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.
In this step, we learned about:
$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.