barbarian meets coding

WebDev, UX & a Pinch of Fantasy

Getting Started With Angular 2 Step by Step: 4 - Routing

| Comments

UPDATE (15th May 2016): This tutorial has been updated to the latest version of Angular (Angular 4). Note that the Angular team has re-branded the terms Angular 2 to Angular and Angular 1.x to AngularJS which are now the official names of these frameworks.

This is the fourth article on the Getting Started with Angular 2 Step by Step series if you missed the previous articles go and check them out!

Good day to you! Four days and four articles! I am on a roll here! :) Yesterday we learned a lot more about Angular 2 data bindings and today you are going to learn about routing, that is, how to navigate between different parts of your application.

Angular 2 Getting Started

The Code Samples

You can download all the code samples for this article from this GitHub repository.

Our Application So Far

At this point in time we have developed a tiny Angular 2 application to learn more about people from the StarWars universe. We have two components, the PeopleListComponent that displays a list of people and the PersonDetailsComponent that displays more information about the character that you have selected.

Now imagine that you are working with a UX designer and he comes to you at tells you: Noooo! What are you doing!? There’s way too much information in the screen, you are overwhelming our users! And you say: take a chill pill… I have a solution! and start redesigning the application and separating it into two views, one for the list of people, and another one for the actual person details.

Enter Angular 2 Routing

Angular 2 gives you the possibility of dividing your application into several views that you can navigate between through the concept of routing. Routing enables you to route the user to different components based on the url that they type on the browser, or that you direct them to through a link.

Angular 2 is designed in a modular fashion so that you can combine it with different libraries if that’s your desire and, hence, routing lives in a different js file than @angular/core. If you take a sneek peak at the package.json of our code sample you’ll be able to see that we refer to it as one of the Angular 2 packages:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
  "dependencies": {
    "@angular/common": "^4.0.0",
    "@angular/compiler": "^4.0.0",
    "@angular/core": "^4.0.0",
    "@angular/forms": "^4.0.0",
    "@angular/http": "^4.0.0",
    "@angular/platform-browser": "^4.0.0",
    "@angular/platform-browser-dynamic": "^4.0.0",
    "@angular/router": "^4.0.0",   // <======= HERE!
    "core-js": "^2.4.1",
    "rxjs": "^5.1.0",
    "zone.js": "^0.8.4"
  },
// ...

If you have worked with AngularJS and ngRoute the rest of the article will feel pretty familiar, the biggest differences being that instead of mapping routes to controllers, Angular 2 maps routes to components, and that Angular 2 style is more declarative. Let’s dive in!

Setting up the PeopleList Route

The way you configure routes in Angular 2 is through a router configuration file with:

  1. A Routes array that will contain a collection of routes
  2. An export that provides the router configuration to the rest of the application.

We’ll start by creating a new file app.routes.ts in the app folder and import the Routes interface from the @angular/router module:

1
import { Routes } from '@angular/router';

Now we can use it to define the default route for our application which will be the list of StarWars people. In order to do that, we import the PeopleListComponent:

1
import { PeopleListComponent } from './people-list.component';

And create the following Routes array:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Route config let's you map routes to components
const routes: Routes = [
  // map '/persons' to the people list component
  {
    path: 'persons',
    component: PeopleListComponent,
  },
  // map '/' to '/persons' as our default route
  {
    path: '',
    redirectTo: '/persons',
    pathMatch: 'full'
  },
];

As you can see above, each route maps a path ('persons') to a component (PeopleListComponent). We tell Angular 2 that this is our default route by creating an additional configuration to map an empty path to the persons path.

The next step is to make our routes available to the rest of the application. In order to achieve that, we import the RouterModule from the @angular/router module:

1
import { Routes, RouterModule }  from '@angular/router';

And export our own defined routes as follows:

1
export const appRouterModule = RouterModule.forRoot(routes);

The whole app.routes.ts route configuration file should now look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Routes, RouterModule } from '@angular/router';
import { PeopleListComponent } from "./people-list/people-list.component";

// Route config let's you map routes to components
const routes: Routes = [
  // map '/persons' to the people list component
  {
    path: 'persons',
    component: PeopleListComponent,
  },
  // map '/' to '/persons' as our default route
  {
    path: '',
    redirectTo: '/persons',
    pathMatch: 'full'
  },
];

export const appRouterModule = RouterModule.forRoot(routes);

Now we can update our application to use the routes that we have defined in our configuration file. The way to do that is by including the routing in our application module app.module.ts:

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
26
27
28
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { PeopleListComponent } from './people-list/people-list.component';
import { PeopleService } from './people.service';
import { PersonDetailsComponent } from './person-details/person-details.component';

import { appRouterModule } from "./app.routes";

@NgModule({
  declarations: [
    AppComponent,
    PeopleListComponent,
    PersonDetailsComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    appRouterModule
  ],
  providers: [PeopleService],
  bootstrap: [AppComponent]
})
export class AppModule { }

This will not only give our app access to the routes we have defined but also to the different routing services provided by @angular/router like Router and ActivatedRoute that we will use later in this article.

Even though we have provided the routes information to our app, we are still using the <people-list> component directly in our AppComponent template. What we really want is for the router to select which component gets displayed based on the path selected in the browser. How do we tell AppComponent to do that? We use the router-outlet directive, an Angular 2 Routing directive that displays the active route (like ng-view).

Update the app.component.html to use the router-outlet directive like this:

1
2
<h1>{{title}}</h1>
<router-outlet></router-outlet>

If you start the application right now (remember ng s -o) you’ll see that the app loads the PeopleListComponent as expected and nothing has changed! Well… if you take a look at the browser url field you should be able to see that it says:

1
http://localhost:3000/persons

That means that the routing is actually working as it should! Good job!

An Aside: Not Using the CLI?

In previous versions of this tutorial running the code samples in a dev server at this point would have resulted in an error. The app would’ve been broken! Oh no! But A closer look at the developer console would’ve shown you this error:

1
2
Subscriber.ts:243 Uncaught EXCEPTION: Error during instantiation of LocationStrategy! (Router -> Location -> LocationStrategy).
ORIGINAL EXCEPTION: No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.

The reason for that is that Angular 2 routing expects you to have a base element in the head section of the index.html:

1
2
3
4
5
6
7
8
<html>
  <head>
    <title>...</title>
    <base href="/">
    <!-- etc... -->
 </head>
<!-- etc... -->
</html>

This element enables the HTML 5 history.pushState api that let’s Angular 2 provide “HTML5 style” URLs (as opposed to using # prefixed ones).

Because we’re now using the Angular cli to generate this project for us from scratch, the base element will always be included and you’ll never run into this error ever again. Another extra cookie for the Angular cli.

Setting up the PersonDetails Route

Now we are going to setup the person details route and we are going to change the workflow of our application, so that, you don’t display the details directly on the same view but in a completely different view.

We start by importing the PersonDetailsComponent and defining the new route in the app.routes.ts:

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
26
27
28
29
30
import { Routes, RouterModule } from '@angular/router';
import { PeopleListComponent } from "./people-list/people-list.component";

// HERE: new import
import { PersonDetailsComponent } from "./person-details/person-details.component";

// Route config let's you map routes to components
const routes: Routes = [
  // map '/persons' to the people list component
  {
    path: 'persons',
    component: PeopleListComponent,
  },

  // HERE: new route for PersonDetailsComponent
  // map '/persons/:id' to person details component
  {
    path: 'persons/:id',
    component: PersonDetailsComponent
  },

  // map '/' to '/persons' as our default route
  {
    path: '',
    redirectTo: '/persons',
    pathMatch: 'full'
  },
];

export const appRouterModule = RouterModule.forRoot(routes);

And now we have defined a Person Details route that maps the /persons/:id path to the PersonDetailsComponent. The :id piece of the route is a route parameter that we’ll use to uniquely identify each Star Wars person.

That means that we need to update our Person interface to include it:

1
2
3
4
5
6
export interface Person {
  id: number;
  name: string;
  height: number;
  weight: number;
}

And our PeopleService service to provide it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Injectable } from '@angular/core';
import { Person } from './person';

@Injectable()
export class PeopleService {
  constructor() { }

  getAll() : Person[] {
    return [
      {id: 1, name: 'Luke Skywalker', height: 177, weight: 70},
      {id: 2, name: 'Darth Vader', height: 200, weight: 100},
      {id: 3, name: 'Han Solo', height: 185, weight: 85},
    ];
  }
}

Creating Route Links

So now we have defined the route but we want our users to be able to access it when they click on a person in the initial view. But how can we craft the right link to a person route?

Angular 2 routing provides the [routerLink] directive that helps you generate these links in a very straightforward fashion.

We will update our PeopleListComponent template to use this [routerLink] directive instead of handling the (click) event as we did before. We will also remove the person-details element from the template:

1
2
3
4
5
6
7
  <ul>
    <li *ngFor="let person of people">
      <a [routerLink]="['/persons', person.id]">
      {{person.name}}
      </a>
    </li>
  </ul>

In this piece of source code above you can see how we bind an array of route parameters to the routerlink directive, one for the path of the route /persons and the other one with the actual id parameter that we defined within app.routes.ts. With those two pieces of information Angular 2 can create the appropriate link to a person (f.i. /person/2).

We can also remove some unnecessary code like the (click) event handler and the selectedPerson property. The updated PeopleListComponent should look like this:

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
26
27
import { Component, OnInit } from '@angular/core';
import { Person } from '../person';
import { PeopleService } from "../people.service";

@Component({
  selector: 'app-people-list',
  template: `
  <!-- this is the new syntax for ng-repeat -->
  <ul>
    <li *ngFor="let person of people">
      <a [routerLink]="['/persons', person.id]">
        {{person.name}}
      </a>
    </li>
  </ul>
  `,
  styleUrls: ['./people-list.component.scss']
})
export class PeopleListComponent implements OnInit {
  people: Person[];

  constructor(private peopleService: PeopleService) { }

  ngOnInit() {
    this.people = this.peopleService.getAll();
  }
}

Ok, so now let’s go back to the browser. If you hover with your mouse over a person’s name you’ll be able to see that it points to the correct link.

If you click though it will not work. That’s because the PersonDetailsComponent component doesn’t know how to retrieve the id from the route, get a person with that id and display it in the view.

Let’s do that next!

Extracting Parameters From Routes

In a previous life the person-details component exposed a person property to which we could bind persons to. In this brave new world or routing-ness that’s not longer the case.

Instead of binding a person directly to it, we are going to access the component through routing. When a user clicks on a person we will navigate to the person details route and the only information available to the component will be a person’s id.

We will update our PersonDetailsComponent to extract that information from the route, retrieve the appropriate user using the PeopleService and then displaying it to our users.

Angular 2 routing provides the ActivatedRoute service for just this purpose, getting access to route parameters. We can import it from the @angular/router module and inject it in our PeopleDetailsComponent via the constructor:

1
import { ActivatedRoute } from '@angular/router';
1
2
3
4
5
6
export class PersonDetailsComponent{
    constructor(private peopleService: PeopleService,
               private route: ActivatedRoute){
    }
    // more codes...
}

And now we can use it to retrieve the id parameter from the url and get the person to display from the PeopleService. We will do that on ngOnInit:

1
2
3
4
5
6
7
8
9
10
11
12
export class PersonDetailsComponent implements OnInit {
    person: Person;

    // more codes...

    ngOnInit(){
        this.route.params.subscribe(params => {
          let id = Number.parseInt(params['id']);
          this.person = this.peopleService.get(id);
        });
    }
}

Notice how the route.params returns an observable, a pattern to handle async operations that we will look more deeply into in the http chapter of these series. In the meantime, know that the subscribe method let’s us execute a bit of code when the async operation, in this case loading a route and retrieving the route params, is complete.

In order to avoid memory leaks we can unsubscribe to the route.params observables when Angular destroys the person details component. We can do that by taking advantage of the OnDestroy lifecycle hook:

1
import { Component, OnInit, OnDestroy } from '@angular/core';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class PersonDetailsComponent implements OnInit {
    // more codes...
    sub: any;

    ngOnInit(){
        this.sub = this.route.params.subscribe(params => {
          let id = Number.parseInt(params['id']);
          this.person = this.peopleService.get(id);
        });
    }

    ngOnDestroy(){
        this.sub.unsubscribe();
    }
}

The complete source code for the PeopleDetailsComponent looks like this:

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
26
27
28
29
30
31
32
33
34
35
36
37
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from "@angular/router";

import { PeopleService } from "../people.service";
import { Person } from "../person";

@Component({
  selector: 'app-person-details',
  template: `
  <section *ngIf="person">
    <h2>You selected:  {{person.name}}</h2>
    <h3>Description</h3>
    <p>
       {{person.name}} weights {{person.weight}} and is {{person.height}} tall.
    </p>
  </section>
  `,
  styles: []
})
export class PersonDetailsComponent implements OnInit, OnDestroy {
  person: Person;
  sub:any;

  constructor(private route:ActivatedRoute,
              private peopleService:PeopleService) { }

  ngOnInit() {
    this.sub = this.route.params.subscribe(params => {
      let id = Number.parseInt(params['id']);
      this.person = this.peopleService.get(id);
    });
  }

  ngOnDestroy(): void {
    this.sub.unsubscribe();
  }
}

We also need to update the PeopleService to provide a new method to retrieve a person by id, the get(id: number) method below will do just that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Injectable } from '@angular/core';
import { Person } from './person';

// 1. Extract array to a PEOPLE variable
const PEOPLE : Person[] = [
      {id: 1, name: 'Luke Skywalker', height: 177, weight: 70},
      {id: 2, name: 'Darth Vader', height: 200, weight: 100},
      {id: 3, name: 'Han Solo', height: 185, weight: 85},
    ];

@Injectable()
export class PeopleService{
  getAll() : Person[] {
    // 2. Refactor to use PEOPLE variable
    return PEOPLE;
  }

  // 3. New method also uses PEOPLE variable
  get(id: number) : Person {
    return PEOPLE.find(p => p.id === id);
  }
}

And we are done! Now if you go and take a look at your app, you click on Luke Skywalker and voilà! You’re shown the intimate details about this mighty Jedi Knight.

But how do you go back to the main view? Well, if you click on the back button of your browser you’ll succeed, Angular 2 Routing works great with the browser default behavior and it’s going to make your app behave in a way in line with what a user would expect.

But what if you wanted to provide a nice button at the bottom of the details for the user to click? (so that I can teach you about navigating programmatically XD)

Going Back to the People List

So now we want to create a button that will allow the user to go back to the main view when he/she/it clicks on it. Angular 2 Routing comes with a Router service that can help you with that.

We start by adding the button to our template and binding it to a goToPeopleList method that we are going to be adding very soon to our component:

1
2
3
4
5
6
7
8
9
  <section *ngIf="person">
    <h2>You selected: {{person.name}}</h2>
    <h3>Description</h3>
    <p>
      {{person.name}} weights {{person.weight}} and is {{person.height}} tall.
    </p>
  </section>
  <! -- NEW BUTTON HERE! -->
  <button (click)="gotoPeoplesList()">Back to peoples list</button>

We can inject the Router service in our PersonDetailsComponent through its constructor. We start by importing it from @angular/router:

1
import { ActivatedRoute, Router} from '@angular/router';

And then we inject it:

1
2
3
4
5
6
7
export class PersonDetailsComponent implements OnInit, OnDestroy {
    // other codes...
    constructor(private peopleService: PeopleService,
                private route: ActivatedRoute,
                private router: Router){
    }
}

The only thing that remains is to implement goToPeopleList to take advantage of the router and trigger the navigation to the main view of the app:

1
2
3
4
5
6
7
8
export class PersonDetailsComponent implements OnInit, OnDestroy {
    // other codes...

    gotoPeoplesList(){
        let link = ['/persons'];
        this.router.navigate(link);
    }
}

Where we call the router.navigate method and send it the parameters necessary for Angular 2 routing to determine where we want to go. In this case, since the route doesn’t require any parameters, we just create an array with the route path /persons.

If you now go to the browser and test the app you’ll see that everything works as expected. You have now learned Angular 2 routing my friend. Good job!!

Here’s the complete source code for our PersonDetailsComponent:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from "@angular/router";

import { PeopleService } from "../people.service";
import { Person } from "../person";

@Component({
  selector: 'app-person-details',
  template: `
  <section *ngIf="person">
    <h2>You selected:  {{person.name}}</h2>
    <h3>Description</h3>
    <p>
       {{person.name}} weights {{person.weight}} and is {{person.height}} tall.
    </p>
  </section>

  <button (click)="gotoPeoplesList()">Back to peoples list</button>
  `,
  styles: []
})
export class PersonDetailsComponent implements OnInit, OnDestroy {
  person: Person;
  sub:any;

  constructor(private route: ActivatedRoute,
              private peopleService: PeopleService,
              private router: Router) { }

  ngOnInit() {
    this.sub = this.route.params.subscribe(params => {
      let id = Number.parseInt(params['id']);
      this.person = this.peopleService.get(id);
    });
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }

  gotoPeoplesList(){
    let link = ['/persons'];
    this.router.navigate(link);
  }
}

And now you may be thinking… wait… if we just navigate to the person list using router, then that means that when I click back on the browser, I am going to go back to the person details. And you would be right. Another option to have a saner browser history is to use the window.history API like below:

1
2
3
gotoPeoplesList(){
    window.history.back();
}

But then I wouldn’t have been able to demonstrate the Router service would I? hehe

Using The Angular CLI to Generate Your Routes

As I hinted in the beginning of the article the Angular cli can help you generate routes for your application. First, when you create a new app from scratch you can run the ng new command with the --routing flag as follows:

1
PS> ng new my-new-app --routing --style scss

The --routing flag will setup a basic routing configuration for your new application in a separate routing module called app-routing.module.ts. This routing configuration will be bundled inside a new Angular module AppRoutingModule:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// define your routes here
const routes: Routes = [
  {
    path: '',
    // don't worry about this
    // we'll explain it when we dive deeper 
    // into routing in future articles of the series
    children: []
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

That is imported by the main Angular module of your app AppModule:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// other imports

import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule // HERE we import the app routing
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

As you can appreciate above the angular cli follows the convention of creating separate modules for application code and routing: An AppModule(app.module.ts) will have a AppRoutingModule (app-routing.module.ts) counterpart.

Indeed, if you create more modules to package different features within your application (which is encouraged in Angular 2) you can create additional routing modules using the Angular cli with the --routing flag. Imagine that you want to add a new feature to our Star Wars application to craft epic stories in the Star wars universe, you could create a new module like this:

1
2
PS> ng generate --routing story-writing
# in short ng g m -r story-writing

Which would result in two new modules in your app, a story-writing.module.ts which would contain the components, services and directives to build that story writing feature, and a story-writing-routing.module.ts which could encapsulate the routing configuration for this part of the application.

Extracting Your Routes Into A Separate Module

Let’s follow up the Angular cli convention and extract our newly created routes into a separate module called AppRoutingModule. It will be helpful because it lets us separate concerns (application logic from routing), it’ll be useful down the line when testing our application and it follows the same convention than the Angular cli to which most developers are already familiar.

Rename the app.routing.ts to app-routing.module.ts and update it’s contents to the following:

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
26
27
28
29
30
31
32
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { PeopleListComponent } from "./people-list/people-list.component";
import { PersonDetailsComponent } from "./person-details/person-details.component";

// Route config let's you map routes to components
const routes: Routes = [
  // map '/persons' to the people list component
  {
    path: 'persons',
    component: PeopleListComponent,
  },
  // map '/persons/:id' to person details component
  {
    path: 'persons/:id',
    component: PersonDetailsComponent
  },
  // map '/' to '/persons' as our default route
  {
    path: '',
    redirectTo: '/persons',
    pathMatch: 'full'
  },
];

// HERE: New module
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

And now we update the app.module.ts to import the AppRoutingModule:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ...lots of imports...

import { AppRoutingModule } from "./app-routing.module";

@NgModule({
  declarations: [
    AppComponent,
    PeopleListComponent,
    PersonDetailsComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule,   // <=== HERE: new routing module
  ],
  providers: [PeopleService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Done! If your run the development server with ng s -o the application should work as expected.

Want to Learn More About Angular 2 Routing?

In this article you’ve learned a lot about Angular 2 routing but you may be thirsting for more. If that’s the case take a look at these articles:

Concluding

Wow, we are almost done with Angular 2 basics! Good job! So far you’ve learned about components, services, dependency injection, Angular 2 different data bindings and template syntax and routing.

Up next! Forms and Validation!!

Comments