Tutorial

Routing Angular 2 Single Page Apps with the Component Router

Draft updated on Invalid Date
Default avatar

By Chris Nwamba

Routing Angular 2 Single Page Apps with the Component Router

This tutorial is out of date and no longer maintained.

Introduction

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

Getting Started: App Setup

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.

  1. 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:

  1. 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:

  1. |--- app
  2. |----- Cats
  3. |------- cat-list.component.ts # Component
  4. |------- cat-details.component.ts # Component
  5. |------- cat.routes.ts # Component routes
  6. |----- Dogs
  7. |------- dog-list.component.ts # Component
  8. |------- dog-details.component.ts # Component
  9. |------- dog.routes.ts # Component routes
  10. |----- app.component.ts # Base Component
  11. |----- app.module.ts # Root Module (final release)
  12. |----- app.routes.ts # Base Route
  13. |----- main.ts # Bootstrapper
  14. |----- pets.service.ts # HTTP Service for fetch API data
  15. |--- index.html # Entry
  16. # 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:

  1. 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;
}

Defining Routes

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.

Creating Placeholder Components

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 {}

Bootstrapping Our Application

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 Main AppComponent

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:

  1. 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:

Going a Little Deeper

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:

  • Landing pages
  • Redirects
  • Route parameters
  • Queries
  • Route restrictions
  • Child routes (maybe)
  • etc

Let’s take some time and have a look at some of these routing features.

Index/Home Page Route and Redirects

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.

Route Parameters

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.

Pet Service to Get Pet Data

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);
  }
}

A Little Bit on HTTP and Dependency Injection

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.

Injecting PetService

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.

Details Components

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

Guarding and Authenticating Routes

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:

  • CanActivate (access route if return value is true)
  • CanDeactivate (leave route if return value is false)

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]
}

Conclusion

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.

Learn more about us


About the authors
Default avatar
Chris Nwamba

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


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!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel