This tutorial is out of date and no longer maintained.
We have been so eager here at Scotch to create a comprehensive guide on Angular 2 Component Router. Now that a reliable version (v3) has been released, it’s a good time then to discuss Component Router.
This article will serve as a guide to implementing routing in Angular 2 with a fully fleshed out example touching major aspects of routing including bootstrapping, configuration, parameterized routes, protecting routes, etc. In the past, we’ve looked at routing in Angular 1.x with ngRoute and UI-Router.
Take a look at what we’ll be building today:
View on Scotch Angular Router on plnkr
Angular 2 uses TypeScript, so if you need a refresher on that, take a look at why TypeScript is your friend and our TypeScript tutorial series.
Before we get started and to save us some setup time, clone the Angular 2 QuickStart then we can build on top of that.
- git clone https://github.com/angular/quickstart scotch-ng-router
The seed already has end-to-end tools to enable you to start building Angular 2 apps. It also comes with all Angular 2 dependencies including angular/router
so we can just pull the packages from npm:
- npm install
We are more concerned with the app
folder of our new project which is simply:
|---app
|-----main.ts # Bootstrapper
|-----app.component.ts # Base Component
|-----app.component.spec.ts # Base Test File
Note: Testing is out of scope for this tutorial so we won’t be making use of app.component.spec.ts
, but we’ll be writing up an article on Angular 2 testing shortly.
Routing requires a lot more files than the above so we will re-structure and organize our app units in folders:
- |--- app
- |----- Cats
- |------- cat-list.component.ts # Component
- |------- cat-details.component.ts # Component
- |------- cat.routes.ts # Component routes
- |----- Dogs
- |------- dog-list.component.ts # Component
- |------- dog-details.component.ts # Component
- |------- dog.routes.ts # Component routes
- |----- app.component.ts # Base Component
- |----- app.module.ts # Root Module (final release)
- |----- app.routes.ts # Base Route
- |----- main.ts # Bootstrapper
- |----- pets.service.ts # HTTP Service for fetch API data
- |--- index.html # Entry
- # Other helper files here
We are not so interested in how the app looks but it won’t hurt to make our app look prettier than what the quickstart offers.
Material Design and Angular are very good friends so we could go ahead to make use of Material Design Lite (MDL). Grab MDL with npm:
- npm install material-design-lite --save
Replace <link rel="stylesheet" href="styles.css">
in ./index.html
with:
<!-- ./index.html -->
<!-- MDL CSS library -->
<link rel="stylesheet" href="/node_modules/material-design-lite/material.min.css">
<!-- MDL JavaScript library -->
<script src="/node_modules/material-design-lite/material.min.js"></script>
<!-- Material Design Icons -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<!-- Custom Style -->
<link rel="stylesheet" href="styles.css">
You can then add some custom styles in the styles.css
:
/** ./style.css **/
/* Jumbotron */
.demo-layout-transparent {
background: linear-gradient(
rgba(0, 0, 255, 0.45),
rgba(0, 0, 255, 0.45)
),
url('assets/scotch-dog1.jpg') center / cover;
height: 400px;
}
/* Nav Bar */
.demo-layout-transparent .mdl-layout__header,
.demo-layout-transparent .mdl-layout__drawer-button {
background: rgba(0, 0, 0, 0.3);
color: white;
}
/* Header Text */
.header-text{
text-align: center;
vertical-align: middle;
line-height: 250px;
color: white;
}
/* Content Wrapper */
.container{
width: 80%;
margin: 0 auto;
}
Let’s get started with configuring a basic route and see how that goes. app.routes.ts
holds the base route configuration and it does not exist yet so we need to create that now:
// ====== ./app/app.routes.ts ======
// Imports
// Deprecated import
// import { provideRouter, RouterConfig } from '@angular/router';
import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CatListComponent } from './cats/cat-list.component';
import { DogListComponent } from './dogs/dog-list.component';
// Route Configuration
export const routes: Routes = [
{ path: 'cats', component: CatListComponent },
{ path: 'dogs', component: DogListComponent }
];
// Deprecated provide
// export const APP_ROUTER_PROVIDERS = [
// provideRouter(routes)
// ];
export const routing: ModuleWithProviders = RouterModule.forRoot(routes);
We import what we need for the base route configuration from @angular/router
and also some components we have yet to create.
We then define an array of routes which is of type Routes
then use RouterModule.forRoot
to export the routes so they can be injected in our application when bootstrapping. The above configuration is basically what it takes to define routes in Angular 2.
The routes config file imports some components that we need to create. For now, we just create them and flesh them out with minimal content, then we can build on them while we move on.
// ====== ./app/Cats/cat-list.component.ts ======
// Import component decorator
import { Component } from '@angular/core';
@Component({
template: `
<h2>Cats</h2>
<p>List of cats</p>`
})
// Component class
export class CatListComponent {}
Just a basic Angular 2 component with a decorator and a component class.
Note: Learn more with this tutorial.
// ====== ./app/Dogs/dog-list.component.ts ======
// Import component decorator
import { Component } from '@angular/core';
@Component({
template: `
<h2>Dogs</h2>
<p>List of dogs</p>`
})
// Component class
export class DogListComponent {}
Before we bootstrap the app, we need to assemble our imports, providers, and declaration using NgModule
so they can be available application-wide. This is a new feature with saves the hustle of managing dependencies in complex projects and makes features (collection of components, services, pipes, and directives that offers a single goal) composable.
// ====== ./app/app.module.ts ======
// Imports
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http';
// Declarations
import { AppComponent } from './app.component';
import { CatListComponent } from './cats/cat-list.component';
import { CatDetailsComponent } from './cats/cat-details.component';
import { DogListComponent } from './dogs/dog-list.component';
import { DogDetailsComponent } from './dogs/dog-details.component';
import { PetService } from './pet.service';
import { Pet } from './pet';
import { routing } from './app.routes';
// Decorator
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
JsonpModule,
routing
],
declarations: [
AppComponent,
CatListComponent,
CatDetailsComponent,
DogListComponent,
DogDetailsComponent
],
providers: [
PetService
],
bootstrap: [ AppComponent ]
})
export class AppModule {
// Module class
}
We are doing great work already in configuring our application. It’s time to bootstrap our application in ./app/main.ts
with the configured routes. Open main.ts
and update with:
// ====== ./app/main.ts ======
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
What this code does is bootstrap our App while injecting our root module during the bootstrap process.
The question is, where is our App? It is yet to be created and we will do that right now:
// ====== ./app/app.component.ts ======
import { Component } from '@angular/core';
// Import router directives
// Deprecated
// import { ROUTER_DIRECTIVES } from '@angular/router';
@Component({
selector: 'my-app',
template: `
<div class="demo-layout-transparent mdl-layout mdl-js-layout">
<header class="mdl-layout__header mdl-layout__header--transparent">
<div class="mdl-layout__header-row">
<!-- Title -->
<span class="mdl-layout-title">Scotch Pets</span>
<!-- Add spacer, to align navigation to the right -->
<div class="mdl-layout-spacer"></div>
<!-- Navigation with router directives-->
<nav class="mdl-navigation">
<a class="mdl-navigation__link" [routerLink]="['/']">Home</a>
<a class="mdl-navigation__link" [routerLink]="['/cats']">Cats</a>
<a class="mdl-navigation__link" [routerLink]="['/dogs']">Dogs</a>
</nav>
</div>
</header>
<main class="mdl-layout__content">
<h1 class="header-text">We care about pets...</h1>
</main>
</div>
<!-- Router Outlet -->
<router-outlet></router-outlet>
`,
// Not necessary as we have provided directives using
// `RouterModule` to root module
// Tell component to use router directives
// directives: [ROUTER_DIRECTIVES]
})
// App Component class
export class AppComponent {}
By configuring and providing the routes using RouterModule
, we also get some important directives including RouterOutlet
(used as router-outlet
) to load route templates and RouterLink
to help with navigation.
The RouterLink directive substitutes the normal href
property and makes it easier to work with route links in Angular 2. It has the following syntax:
<!-- Watch out for the quotes and braces -->
<a [routerLink]="['/url']">Url Title</a>
The RouterOutlet directive is used to display views for a given route. This is where templates of specific routes are loaded while we navigate:
<router-outlet></router-outlet>
Angular makes it easy to make our SPA route URLs look indistinguishable form sever-served URLs. All we need to do is set base URL in the index.html
:
<!-- ./index.html -->
<base href="/">
Let’s see how far we have gone by running the app:
- npm start
We do not have an index route (/
) and that will throw errors to the console. Ignore it (will fix that) and navigate to /cats
or /dogs
:
Yes, we have a functional route, but we all know that real-life applications require a bit more than a simple route. Real apps have index/home page routes for:
Let’s take some time and have a look at some of these routing features.
The first and most important is to fix our index route. I can’t think of any relevant information to put in the index route so what we can do is redirect to /dogs
once the index route is hit.
// ====== ./app/app.routes.ts ======
// redirect for the home page
// Route Configuration
export const routes: Routes = [
{
path: '',
redirectTo: '/dogs',
pathMatch: 'full'
},
{ path: 'cats', component: CatListComponent },
{ path: 'dogs', component: DogListComponent }
];
We just successfully killed two birds with one stone. We have an index route and we have also seen how we can redirect to another route. If you prefer to have a component to the index route, configure as follows:
// ====== ./app/app.routes.ts ======
// component for the index/home page
// Route Configuration
export const routes: Routes = [
{
path: '',
component: HomeComponent // Remember to import the Home Component
},
{ path: 'cats', component: CatListComponent },
{ path: 'dogs', component: DogListComponent }
];
When making a redirect it is important to tell the router how to match the URL. There are two options for that - full
or prefix
. full
matches the URL as it is while prefix
matches URL prefixed with the redirect path.
This is a good time to add more features to the demo app by fetching the list of pets from a remote server and retrieving each pet’s details with their ID. This will give us a chance to see how route parameters work.
It’s a good practice to isolate heavy tasks from our controllers using services. A service is a data class provider that makes a request (not necessarily an HTTP call) for data when a component needs to make use of it:
// ====== ./app/pet.service.ts ======
// Imports
import { Injectable } from '@angular/core';
import { Jsonp, URLSearchParams } from '@angular/http';
// Decorator to tell Angular that this class can be injected as a service to another class
@Injectable()
export class PetService {
// Class constructor with Jsonp injected
constructor(private jsonp: Jsonp) { }
// Base URL for Petfinder API
private petsUrl = 'http://api.petfinder.com/';
// Get a list if pets based on animal
findPets(animal : string) {
// End point for list of pets:
// http://api.petfinder.com/pet.find?key=[API_KEY]&animal=[ANIMAL]&format=json&location=texas
const endPoint = 'pet.find'
// URLSearchParams makes it easier to set query parameters and construct URL
// rather than manually concatenating
let params = new URLSearchParams();
params.set('key', '[API_KEY]');
params.set('location', 'texas');
params.set('animal', animal);
params.set('format', 'json');
params.set('callback', 'JSONP_CALLBACK');
// Return response
return this.jsonp
.get(this.petsUrl + endPoint, { search: params })
.map(response => <string[]> response.json().petfinder.pets.pet);
}
// get a pet based on their id
findPetById(id: string) {
// End point for list of pets:
// http://api.petfinder.com/pet.find?key=[API_KEY]&animal=[ANIMAL]&format=json&location=texas
const endPoint = 'pet.get'
// URLSearchParams makes it easier to set query parameters and construct URL
// rather than manually concatinating
let params = new URLSearchParams();
params.set('key', '[API_KEY]');
params.set('id', id);
params.set('format', 'json');
params.set('callback', 'JSONP_CALLBACK');
// Return response
return this.jsonp
.get(this.petsUrl + endPoint, { search: params })
.map(response => <string[]> response.json().petfinder.pet);
}
}
HTTP and DI are beyond the scope of this article (though coming soon) but a little explanation of what is going on won’t cause us harm.
The class is decorated with an @Injectable
decorator which tells Angular that this class is meant to be used as a provider to other components. JSONP rather than HTTP is going to be used to make API requests because of CORS so we inject the service into PetService
.
The class has 3 members - a private property that just holds the base URL of the Petfinder API, a method to retrieve the list of pets based on the type, and another method to get a pet by its ID.
PetService was not built to run on it’s own, rather we need to inject the service into our existing list components:
// Imports
import { Component, OnInit } from '@angular/core';
import { PetService } from '../pet.service'
import { Observable } from 'rxjs/Observable';
import { ROUTER_DIRECTIVES } from '@angular/router';
@Component({
template: `
<h2>Dogs</h2>
<p>List of dogs</p>
<ul class="demo-list-icon mdl-list">
<li class="mdl-list__item" *ngFor="let dog of dogs | async">
<span class="mdl-list__item-primary-content">
<i class="material-icons mdl-list__item-icon">pets</i>
<a [routerLink]="['/dogs', dog.id.$t]">{{ dog.name.$t }}</a>
</span>
</li>
</ul>
`,
// Providers
// Already provided in the root module
//providers: [PetService]
})
// Component class implementing OnInit
export class DogListComponent implements OnInit {
// Private property for binding
dogs: Observable<string[]>;
constructor(private petService: PetService) {
}
// Load data ones componet is ready
ngOnInit() {
// Pass retreived pets to the property
this.dogs = this.petService.findPets('dog');
}
}
Note: The trailing .$t
is a result of the API structure and not an Angular thing so you do not have to worry about that.
We are binding an observable type, dogs
to the view and looping through it with the NgFor
directive. The component class extends OnInit
which when its ngOnInit
method is overridden, is called once the component loads.
A VERY important thing to also note is the routerLink
again. This time it does not just point to /dog
but has a parameter added
<a [routerLink]="['/dogs', dog.id.$t]">{{dog.name.$t}}</a>
CatListComponent
looks quite exactly like DogListComponent
so I will leave that to you to complete.
The link from the list components points to a non-existing route. The route’s component is responsible for retrieving specific pet based on an id. The first thing to do before creating these components is to make there routes. Back to the app.routes
:
// ====== ./app/app.routes.ts ======
// Imports
// Deprecated import
// import { provideRouter, RouterConfig } from '@angular/router';
import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { dogRoutes } from './dogs/dog.routes';
// Route Configuration
export const routes: Routes = [
{
path: '',
redirectTo: '/dogs',
pathMatch: 'full'
},
// Add dog routes form a different file
...dogRoutes
];
// Deprecated provide
// export const APP_ROUTER_PROVIDERS = [
// provideRouter(routes)
// ];
export const routing: ModuleWithProviders = RouterModule.forRoot(routes);
For modularity, the dog-related routes have been moved to a different file and then we import and add it to the base route using the spread operator. Our dog.routes
now looks like:
// ======= ./app/Dogs/dog.routes.ts =====
// Imports
// Deprecated import
// import { RouterConfig } from '@angular/router';
import { Routes } from '@angular/router';
import { DogListComponent } from './dog-list.component';
import { DogDetailsComponent } from './dog-details.component';
// Route Configuration
export const dogRoutes: Routes = [
{ path: 'dogs', component: DogListComponent },
{ path: 'dogs/:id', component: DogDetailsComponent }
];
There is now a DogDetailsComponent
as you can now see but the component is yet to be created. The component will receive id
parameter form the URL and use the parameter to query the API for a pet:
// ====== ./app/Dogs/dog-details.component ======
// Imports
import { Component, OnInit } from '@angular/core';
import { PetService } from '../pet.service'
import { Observable } from 'rxjs/Observable';
import { ROUTER_DIRECTIVES, ActivatedRoute } from '@angular/router';
import { Pet } from '../pet';
@Component({
template: `
<div *ngIf="dog">
<h2>{{dog.name.$t}}</h2>
<img src="{{dog.media.photos.photo[3].$t}}"/>
<p><strong>Age: </strong>{{dog.age.$t}}</p>
<p><strong>Sex: </strong>{{dog.sex.$t}}</p>
<p><strong>Description: </strong>{{dog.description.$t}}</p>
</div>
`
})
// Component class implementing OnInit
export class DogDetailsComponent implements OnInit {
// Private properties for binding
private sub:any;
private dog:string[];
constructor(private petService: PetService, private route: ActivatedRoute) {
}
// Load data ones componet is ready
ngOnInit() {
// Subscribe to route params
this.sub = this.route.params.subscribe(params => {
let id = params['id'];
// Retrieve Pet with Id route param
this.petService.findPetById(id).subscribe(dog => this.dog = dog);
});
}
ngOnDestroy() {
// Clean sub to avoid memory leak
this.sub.unsubscribe();
}
}
What is important to keep an eye on is that we are getting the ID form the route URL using ActivatedRoute
. The ID is passed into the PetService
’s findPetById
method to fetch a single pet. It might seem like a lot of work of having to subscribe to route params but there is more to it. Subscribing this way makes it easier to unsubscribe once we exit the component in ngOnDestroy
thereby cutting the risks of memory leak.
Now it’s time to take it as a challenge to complete that of CatDetailsComponent
and CatRoutes
though you can find them in the demo if you get stuck.
Let’s take a look at one more thing we can do with component router
Sometimes we might have sensitive information to protect from certain categories of users that have access to our application. We might not want a random user to create pets, edit pets or delete pets.
Showing them these views when they cannot make use of it does not make sense so the best thing to do is to guard them.
Angular has two guards:
This is how we can make use of the guard:
import { CanActivate } from '@angular/router';
export class AuthGuard implements CanActivate {
canActivate() {
// Imaginary method that is supposed to validate an auth token
// and return a boolean
return tokenExistsAndNotExpired();
}
}
It’s a class that implements router’s CanActivate
and overrides canActivate
method. What you can do with with the service is supply as array value to canActivate
property of route configuration:
{
path: 'admin',
component: PetAdmin,
canActivate: [AuthGuard]
}
There is room to learn a lot more on Angular Component Router but what we have seen is enough to guide you through what you need to start taking advantage of routing in an Angular 2 application.
More to come from Scotch and Scotch-School on Angular 2, do watch out so you don’t miss.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!