In this step, you will learn:
The goal for this step is to add one more page to the app that shows the details of the selected party.
By default we have a list of parties shown on the page, but when a user clicks on a list item, the app should navigate to the new page and show selected party details.
Since we want to have multiple views in our app we have to move the current list of parties into the separate component.
Let's move the content of AppComponent in app.component.ts
out into a PartiesList
component.
Create a new file called parties-list.component.ts
and put it in client/imports/app/parties
directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Parties } from '../../../../both/collections/parties.collection';
import { Party } from '../../../../both/models/party.model';
import template from './parties-list.component.html';
@Component({
selector: 'parties-list',
template
})
export class PartiesListComponent {
parties: Observable<Party[]>;
constructor() {
this.parties = Parties.find({}).zone();
}
removeParty(party: Party): void {
Parties.remove(party._id);
}
}
There are few things we did in that step:
Parties
collectionparties-list
as the selector instead of app
Now we can copy app.component.html
into the parties
directory and rename it parties-list.component.html
:
1
2
3
4
5
6
7
8
9
10
11
12
<div>
<parties-form></parties-form>
<ul>
<li *ngFor="let party of parties | async">
{{party.name}}
<p>{{party.description}}</p>
<p>{{party.location}}</p>
<button (click)="removeParty(party)">X</button>
</li>
</ul>
</div>
Also, let's clean-up app.component.ts
to prepare it for the next steps:
1
2
3
4
6
7
8
9
import { Component } from '@angular/core';
import template from './app.component.html';
...some lines skipped...
selector: 'app',
template
})
export class AppComponent {}
and the template for it, which is app.component.html
:
You will notice that the interface of your app has disappeared. But don't worry! It will come back later on.
1
<div></div>
And let's add the new Component to the index file:
1
2
3
4
5
6
7
import { PartiesFormComponent } from './parties-form.component';
import { PartiesListComponent } from './parties-list.component';
export const PARTIES_DECLARATIONS = [
PartiesFormComponent,
PartiesListComponent
];
@angular/router
is the package in charge of Routing in Angular 2, and we will learn how to use it now.
This package provides utils to define our routes, and get them as NgModule
object we just include in our application.
Defining routes
We need to create an array of route definitions. The Route
interface comes with help. This way we can be sure that properties of that object are correctly used.
The very basic two properties are path
and component
. The path is to define the url and the other one is to bind a component to it.
We will export our routes using routes
variable.
Let's warp it in the app.routes.ts
file, here's what it suppose to look like:
1
2
3
4
5
6
7
import { Route } from '@angular/router';
import { PartiesListComponent } from './parties/parties-list.component';
export const routes: Route[] = [
{ path: '', component: PartiesListComponent }
];
Now we can use routes
in the NgModule
, with the RouteModule
provided by Angular 2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { routes } from './app.routes';
import { PARTIES_DECLARATIONS } from './parties';
@NgModule({
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
RouterModule.forRoot(routes)
],
declarations: [
AppComponent,
Our app still has to display the view somewhere. We'll use routerOutlet
component to do this.
1
2
3
<div>
<router-outlet></router-outlet>
</div>
Now, because we use a router that based on the browser path and URL - we need to tell Angular 2 router which path is the base path.
We already have it because we used the Angular 2 boilerplate, but if you are looking for it - you can find it in client/index.html
file:
<base href="/">
Let's add another view to the app: PartyDetailsComponent
. Since it's not possible yet to get party details in this component, we are only going to make stubs.
When we're finished, clicking on a party in the list should redirect us to the PartyDetailsComponent for more information.
1
2
3
4
5
6
7
8
9
import { Component } from '@angular/core';
import template from './party-details.component.html';
@Component({
selector: 'party-details',
template
})
export class PartyDetailsComponent {}
And add a simple template outline for the party details:
1
2
3
4
5
<header>
<h2>PARTY_NAME</h2>
<p>PARTY_DESCRIPTION</p>
</header>
And let's add the new Component to the index file:
1
2
3
4
5
6
7
8
9
import { Route } from '@angular/router';
import { PartiesListComponent } from './parties/parties-list.component';
import { PartyDetailsComponent } from './parties/party-details.component';
export const routes: Route[] = [
{ path: '', component: PartiesListComponent },
{ path: 'party/:partyId', component: PartyDetailsComponent }
];
1
2
3
4
5
6
7
8
9
import { PartiesFormComponent } from './parties-form.component';
import { PartiesListComponent } from './parties-list.component';
import { PartyDetailsComponent } from './party-details.component';
export const PARTIES_DECLARATIONS = [
PartiesFormComponent,
PartiesListComponent,
PartyDetailsComponent
];
Now we can define the route:
1
2
3
4
5
6
7
8
9
import { Route } from '@angular/router';
import { PartiesListComponent } from './parties/parties-list.component';
import { PartyDetailsComponent } from './parties/party-details.component';
export const routes: Route[] = [
{ path: '', component: PartiesListComponent },
{ path: 'party/:partyId', component: PartyDetailsComponent }
];
1
2
3
4
5
6
7
8
9
import { PartiesFormComponent } from './parties-form.component';
import { PartiesListComponent } from './parties-list.component';
import { PartyDetailsComponent } from './party-details.component';
export const PARTIES_DECLARATIONS = [
PartiesFormComponent,
PartiesListComponent,
PartyDetailsComponent
];
As you can see, we used :partyId
inside of the path string. This way we define parameters. For example, localhost:3000/party/12
will point to the PartyDetailsComponent with 12
as the value of the partyId
parameter.
We still have to add a link that redirects to party details.
Let's add links to the new router details view from the list of parties.
As we've already seen, each party link consists of two parts: the base PartyDetailsComponent
URL and a party ID, represented by the partyId
in the configuration. There is a special directive called routerLink
that will help us to compose each URL.
Now we can wrap our party in a routerLink
and pass in the _id as a parameter. Note that the id is auto-generated when an item is inserted into a Mongo Collection.
3
4
5
6
7
8
9
<ul>
<li *ngFor="let party of parties | async">
<a [routerLink]="['/party', party._id]">{{party.name}}</a>
<p>{{party.description}}</p>
<p>{{party.location}}</p>
<button (click)="removeParty(party)">X</button>
As you can see, we used an array. The first element is a path that we want to use and the next one is to provide a value of a parameter.
You can provide more than one parameter by adding more elements into an array.
We've just added links to the PartyDetails
view.
The next thing is to grab the partyId
route parameter in order to load the correct party in the PartyDetails
view.
In Angular 2, it's as simple as passing the ActivatedRoute
argument to the PartyDetails
constructor:
1
2
3
4
5
6
7
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import 'rxjs/add/operator/map';
import template from './party-details.component.html';
...some lines skipped...
selector: 'party-details',
template
})
export class PartyDetailsComponent implements OnInit {
partyId: string;
constructor(
private route: ActivatedRoute
) {}
ngOnInit() {
this.route.params
.map(params => params['partyId'])
.subscribe(partyId => this.partyId = partyId);
}
}
We used another RxJS feature called
map
- which transform the stream of data into another object - in this case, we want to get thepartyId
from theparams
, then we subscribe to the return value of this function - and the subscription will be called only with thepartyId
that we need.As you might noticed, Angular 2 uses RxJS internally and exposes a lot of APIs using RxJS Observable!
Dependency injection is employed heavily here by Angular 2 to do all the work behind the scenes.
TypeScript first compiles this class with the class metadata that says what argument types this class expects in the constructor (i.e. ActivatedRoute
),
so Angular 2 knows what types to inject if asked to create an instance of this class.
Then, when you click on a party details link, the router-outlet
directive will create a ActivatedRoute
provider that provides
parameters for the current URL. Right after that moment if a PartyDetails
instance is created by means of the dependency injection API, it's created with ActivatedRoute
injected and equalled to the current URL inside the constructor.
If you want to read more about dependency injection in Angular 2, you can find an extensive overview in this article. If you are curious about class metadata read more about it here.
In order to avoid memory leaks and performance issues, we need to make sure that every time we use subscribe
in our Component - we also use unsubscribe
when the data is no longer interesting.
In order to do so, we will use Angular 2 interface called OnDestroy
and implement ngOnDestroy
- which called when our Component is no longer in the view and removed from the DOM.
So let's implement this:
1
2
3
4
5
6
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/map';
...some lines skipped...
selector: 'party-details',
template
})
export class PartyDetailsComponent implements OnInit, OnDestroy {
partyId: string;
paramsSub: Subscription;
constructor(
private route: ActivatedRoute
) {}
ngOnInit() {
this.paramsSub = this.route.params
.map(params => params['partyId'])
.subscribe(partyId => this.partyId = partyId);
}
ngOnDestroy() {
this.paramsSub.unsubscribe();
}
}
Now, we need to get the actual Party
object with the ID we got from the Router, so let's use the Parties
collection to get it:
1
2
3
4
5
6
7
8
9
10
11
12
16
17
18
19
20
21
22
25
26
27
28
29
30
31
32
33
34
35
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/map';
import { Parties } from '../../../../both/collections/parties.collection';
import { Party } from '../../../../both/models/party.model';
import template from './party-details.component.html';
@Component({
...some lines skipped...
export class PartyDetailsComponent implements OnInit, OnDestroy {
partyId: string;
paramsSub: Subscription;
party: Party;
constructor(
private route: ActivatedRoute
...some lines skipped...
ngOnInit() {
this.paramsSub = this.route.params
.map(params => params['partyId'])
.subscribe(partyId => {
this.partyId = partyId
this.party = Parties.findOne(this.partyId);
});
}
ngOnDestroy() {
findOne
return the actual object instead of returning Observable or Cursor.
In our next step we will display the party details inside our view!
Add a link back to the PartiesList
component from PartyDetails
.
Let's list what we've accomplished in this step: