Getting Started with Angular 2 Step by Step: 4 - Routing
UPDATE (13th October 2017): This Angular 2 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.
The Code Samples
You can download all the code samples for this article from this GitHub repository. You can also check the unbelievably cool online editor Stackblitz which has awesome support for writing Angular prototypes on the web. My recommendation is that you follow the tutorial along using your own development environment, that way you get to enjoy the full Angular developer experience. But if you don’t have time or energy to set it up right now, then try Stackblitz.
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 module than @angular/core
. If you take a sneak 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:
// ...
"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:
- A
Routes
array that will contain a collection of routes - 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:
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
:
import { PeopleListComponent } from './people-list/people-list.component'
And create the following Routes
array:
// 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:
import { Routes, RouterModule } from '@angular/router'
And export our own defined routes as follows:
export const appRouterModule = RouterModule.forRoot(routes)
The whole app.routes.ts
route configuration file should now look like this:
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
:
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:
<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:
http://localhost:3000/persons
That means that the routing is actually working as it should! Good job!
An Aside: Following This Angular 2 Tutorial Without The Angular 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:
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
:
<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
:
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:
export interface Person {
id: number
name: string
height: number
weight: number
}
And our PeopleService
service to provide it:
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 and 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:
<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:
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 PersonDetailsComponent
via the constructor:
import { ActivatedRoute } from '@angular/router'
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
:
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 an 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:
import { Component, OnInit, OnDestroy } from '@angular/core'
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 PersonDetailsComponent
looks like this:
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:
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:
<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
:
import { ActivatedRoute, Router } from '@angular/router'
And then we inject it:
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:
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
:
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:
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:
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
:
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
:
// 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:
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:
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
:
// ...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:
- Angular 2 Routing straight from the Angular 2 docs with a great coding sample
- Angular Router by Victor Savkin updated with the latest Angular 2 router
- Angular Router: Empty Paths, Componentless Routes and Redirects also by Viktor Savkin with extra information about corner cases for the Angular 2 router
- New Book on Angular 2 Routing by its designer Viktor Savkin
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.
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.