Routing & Multiple Views

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

In this step, you will learn:

  • how to create a layout template
  • how to build an app that has multiple views with the new Angular router.

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.

Parties List

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.

6.1 Create PartiesList component client/imports/app/parties/parties-list.component.ts
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 [email protected]/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:

  • Updated path of the module with Parties collection
  • Changed the name of the template
  • Used parties-list as the selector instead of app
  • Renamed the class

Now we can copy app.component.html into the parties directory and rename it parties-list.component.html:

6.2 Copy contents of app.html to PartiesList template client/imports/app/parties/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:

6.3 Clean up App component client/imports/app/app.component.ts
1
2
3
4
 
6
7
8
9
import { Component } from [email protected]/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.

6.4 Clean up App template client/imports/app/app.component.html
1
<div></div>

And let's add the new Component to the index file:

6.5 Add PartiesList to parties declarations client/imports/app/parties/index.ts
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
];

Routing

@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 [email protected]/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:

6.7 Register RouterModule client/imports/app/app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { NgModule } from [email protected]/core';
import { BrowserModule } from [email protected]/platform-browser';
import { FormsModule, ReactiveFormsModule } from [email protected]/forms';
import { RouterModule } from [email protected]/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.

6.8 Implement routerOutlet client/imports/app/app.component.html
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="/">

Parties details

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 [email protected]/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:

6.10 Create template for PartyDetails client/imports/app/parties/party-details.component.html
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:

{{{diff_step 6.11} filename="client/imports/app/parties/index.ts"}}

Now we can define the route:

{{{diff_step 6.11} filename="client/imports/app/app.routes.ts"}}

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.

RouterLink

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.

6.12 Use routerLink in PartiesList component client/imports/app/parties/parties-list.component.html
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.

Injecting Route Params

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:

6.13 Subscribe to get the partyId client/imports/app/parties/party-details.component.ts
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 [email protected]/core';
import { ActivatedRoute } from [email protected]/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 the partyId from the params, then we subscribe to the return value of this function - and the subscription will be called only with the partyId 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:

6.14 Unsubscribe when component is being destroyed client/imports/app/parties/party-details.component.ts
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 [email protected]/core';
import { ActivatedRoute } from [email protected]/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 [email protected]/core';
import { ActivatedRoute } from [email protected]/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!

Challenge

Add a link back to the PartiesList component from PartyDetails.

Summary

Let's list what we've accomplished in this step:

  • split our app into two main views
  • configured routing to use these views and created a layout template
  • learned briefly how dependency injection works in Angular 2
  • injected route parameters and loaded party details with the ID parameter