JavaScript - - By Jurgen Van de Moere

An Introduction to Component Routing with Angular Router

This article is part 4 of the SitePoint Angular 2+ Tutorial on how to create a CRUD App with the Angular CLI.


  1. Part 0— The Ultimate Angular CLI Reference Guide
  2. Part 1— Getting our first version of the Todo application up and running
  3. Part 2— Creating separate components to display a list of todo’s and a single todo
  4. Part 3— Update the Todo service to communicate with a REST API
  5. Part 4— Use Angular router to resolve data
  6. Part 5— Add authentication to protect private content

For expert-led online Angular training courses you can’t go past Ultimate Angular by Todd Motto. Try his courses here, and use discount code SITEPOINT to get 25% off and to help support SitePoint.


In part one we learned how to get our Todo application up and running and deploy it to GitHub pages. This worked just fine but, unfortunately, the whole app was crammed into a single component.

In part two we examined a more modular component architecture and learned how to break this single component into a structured tree of smaller components that are easier to understand, reuse and maintain.

In part three we updated our application to communicate with a REST API backend using RxJS and Angular’s HTTP service.

In this part, we will introduce Angular router and learn how it can update our application when the browser URL changes and vice versa. We will also learn how we can update our application to resolve data from our backend API using the router.

Don’t worry! You don’t need to have followed part one, two or three of this tutorial, for four to make sense. You can simply grab a copy of our repo, checkout the code from part three, and use that as a starting point. This is explained in more detail below.

Up and Running

Make sure you have the latest version of the Angular CLI installed. If you don’t, you can install it with the following command:

npm install -g @angular/cli@latest

If you need to remove a previous version of the Angular CLI, you can:

npm uninstall -g @angular/cli angular-cli
npm cache clean
npm install -g @angular/cli@latest

After that, you’ll need a copy of the code from part three. This is available at https://github.com/sitepoint-editors/angular-todo-app. Each article in this series has a corresponding tag in the repository so you can switch back and forth between the different states of the application.

The code that we ended with in part three and that we start with in this article is tagged as part-3. The code that we end this article with is tagged as part-4.

You can think of tags like an alias to a specific commit id. You can switch between them using git checkout. You can read more on that here.

So, to get up and running (the latest version of the Angular CLI installed) we would do:

git clone git@github.com:sitepoint-editors/angular-todo-app.git
cd angular-todo-app
git checkout part-3
npm install
ng serve

Then visit http://localhost:4200/. If all is well, you should see the working Todo app.

A quick recap

Here is what our application architecture looked like at the end of part 3:

Application Architecture

In this article we will:

  • learn why an application may need routing
  • learn what a JavaScript router is
  • learn what Angular router is, how it works and what it can do for you
  • set up Angular router and configure the routes for our application
  • create a resolver to fetch the todo’s from our REST API
  • update our application to fetch the todo’s using our new resolver

By the end of this article, you will understand:

  • when and why your application may need routing
  • the difference between routing on the server and routing in the browser
  • what Angular router is and what it can do for your application
  • how to set up Angular router
  • how to configure routes for your application
  • how to tell Angular router where to place components in the DOM
  • how to gracefully handle unknown URLs
  • what a resolver is and what it can be used for
  • how to use a resolver to resolve data using Angular router

So, let’s get started!

Why routing?

In its current state, our web application does not take the browser URL into account.

We access our application through one URL e.g. http://localhost:4200 and our application is not aware of any other URLs such as http://localhost:4200/todos.

Most web applications need to support different URLs to navigate users to different pages in the application. That is where a router comes in.

In traditional websites, routing is handled by a router on the server:

  1. a user clicks a link in the browser, causing the URL to change
  2. the browser sends an HTTP request to server
  3. the server reads the URL from the HTTP request and generates an appropriate HTTP response
  4. the server sends the HTTP response to the browser

In modern JavaScript web applications, routing is often handled by a JavaScript router in the browser.

What is a JavaScript router?

In essence, a JavaScript router does 2 things:

  1. update the web application state when the browser URL changes
  2. update the browser URL when the web application state changes

JavaScript routers make it possible for us to develop Single Page Applications (SPA’s).

A Single Page Application is a web application that provides a user experience similar to a desktop application. In a Single Page Application, all communication with a back-end occurs behind the scenes.

When a user navigates from one page to another, the page is updated dynamically without reload, even if the URL changes.

There are many different JavaScript router implementations available.

Some of them are specifically written for a certain JavaScript framework such as Angular, ember, React, Vue.js, aurelia, etc. Other implementations are built for generic purposes and are not tied to a specific framework.

What is Angular router?

Angular router is an official Angular routing library, written and maintained by the Angular Core Team.

It is a JavaScript router implementation that is designed to work with Angular and is packaged as @angular/router.

First of all, Angular router takes care of the duties of a JavaScript router:

  • it activates all required Angular components to compose a page when a user navigates to a certain URL
  • it lets users navigate from one page to another without page reload
  • it updates the browser’s history so the user can use the back and forward buttons when navigating back and forth between pages

In addition, Angular router allows us to:

  • redirect a URL to another URL
  • resolve data before a page is displayed
  • run scripts when a page is activated or deactivated
  • lazy load parts of our application

In this article, we will learn how to set up and configure Angular router, how to redirect a URL and how to use Angular router to resolve todo’s from our back-end API.

In the next article, we will add authentication to our application and use the router to make sure some of the pages can only be accessed when the user is signed in.

How Angular Router Works

Before we dive into the code, it is important to understand how Angular router operates and the terminology it introduces.

When a user navigates to a page, Angular router performs the following steps in order:

  1. it reads the browser URL the user wants to navigate to
  2. it applies a URL redirect (if one is defined)
  3. it figures out which router state corresponds to the URL
  4. it runs the guards that are defined in the router state
  5. it resolves the required data for the router state
  6. it activates the Angular components to display the page
  7. it manages navigation and repeats the steps above when a new page is requested

To accomplish its tasks, Angular router introduces the following terms and concepts:

  • router service: the global Angular router service in our application
  • router configuration: definition of all possible router states our application can be in
  • router state: the state of the router at some point in time, expressed as a tree of activated route snapshots
  • activated route snapshot: provides access to the URL, parameters, and data for a router state node
  • guard: script that runs when a route is loaded, activated or deactivated
  • resolver: script that fetches data before the requested page is activated
  • router outlet: location in the DOM where Angular router can place activated components

Don’t worry if the terminology sounds overwhelming. You will get used to the terms as we tackle them gradually in this series and as you gain more experience with Angular router.

An Angular application that uses Angular router only has one router service instance; It is a singleton. Whenever and wherever you inject the Router service in your application, you will get access to the same Angular router service instance.

For a more in-depth look at Angular routing process, make sure to check out the 7-step routing process of Angular router navigation.

Enabling Routing

To enable routing in our Angular application, we need to do 3 things:

  1. create a routing configuration that defines the possible states for our application
  2. import the routing configuration into our application
  3. add a router outlet to tell Angular router where to place the activated components in the DOM

So let’s start by creating a routing configuration.

Creating the routing configuration

To create our routing configuration, we need a list of the URLs we would like our application to support.

Currently, our application is very simple and only has one page that shows a list of todo’s:

  • /: show list of todo’s

which would show the list of todo’s as the homepage of our application.

However, when a user bookmarks / in their browser to consult their list of todo’s and we change the contents of our homepage (which we will do in part 5 of this series), their bookmark would no longer show their list of todo’s.

So let’s give our todo list its own URL and redirect our homepage to it:

  • /: redirect to /todos
  • /todos: show list of todo’s

This provides us with two benefits:

  • when users bookmark the todos page, their browser will bookmark /todos instead of /, which will keep working as expected, even if we change the home page contents
  • we can now easily change our homepage by redirecting it to any URL we like, which is convenient if you need to change your homepage contents regularly

The official Angular style guide recommends storing the routing configuration for an Angular module in a file with a filename ending in -routing.module.ts that exports a separate Angular module with a name ending in RoutingModule.

Our current module is called AppModule, so we create a file src/app/app-routing.module.ts and export our routing configuration as an Angular module called AppRoutingModule:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AppComponent } from './app.component';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'todos',
    pathMatch: 'full'
  },
  {
    path: 'todos',
    component: AppComponent
  }
];

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

First we import RouterModule and Routes from @angular/router:

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

Next, we define a variable routes of type Routes and assign it our router configuration:

const routes: Routes = [
  {
    path: '',
    redirectTo: 'todos',
    pathMatch: 'full'
  },
  {
    path: 'todos',
    component: AppComponent
  }
];

The Routes type is optional and lets an IDE with TypeScript support or the TypeScript compiler conveniently validate your route configuration during development.

The router configuration represents all possible router states our application can be in.

It is a tree of routes, defined as a JavaScript array, where each route can have the following properties:

  • path: string, path to match the URL
  • patchMatch: string, how to match the URL
  • component: class reference, component to activate when this route is activated
  • redirectTo: string, URL to redirect to when this route is activated
  • data: static data to assign to route
  • resolve: dynamic data to resolve and merge with data when resolved
  • children: child routes

Our application is simple and only contains two sibling routes, but a larger application could have a router configuration with child routes such as:

const routes: Routes = [
  {
    path: '',
    redirectTo: 'todos',
    pathMatch: 'full'
  },
  {
    path: 'todos',
    children: [
      {
        path: '',
        component: 'TodosPageComponent'
      },
      {
        path: ':id',
        component: 'TodoPageComponent'
      }
    ]
  }
];

where todos has two child routes and :id is a route parameter, enabling the router to recognize the following URL’s:

  • /: homepage, redirect to /todos
  • /todos: activate TodosPageComponent and show list of todo’s
  • /todos/1: activate TodoPageComponent and set value of :id parameter to 1
  • /todos/2: activate TodoPageComponent and set value of :id parameter to 2

Notice how we specify patchMatch: 'full' when defining the redirect.

Angular router has two matching strategies:

  • prefix: default, matches when the URL starts with the value of path
  • full: matches when the URL equals the value of path

If we create the following route:

// no pathMatch specified, so Angular router applies
// the default `prefix` pathMatch
{
  path: '',
  redirectTo: 'todos'
}

then Angular router applies the default prefix path matching strategy and every URL is redirected to todos because every URL starts with the empty string '' specified in path.

We only want our homepage to be redirected to todos , so we add pathMatch: 'full' to make sure that only the URL that equals the empty string '' is matched:

{
  path: '',
  redirectTo: 'todos',
  pathMatch: 'full'
}

To learn more about the different routing configuration options, check out the official Angular documentation on Routing and Navigation.

Finally, we create and export an Angular module AppRoutingModule:

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

There are two ways to create a routing module:

  1. RouterModule.forRoot(routes): creates a routing module that includes the router directives, the route configuration and the router service
  2. RouterModule.forChild(routes): creates a routing module that includes the router directives, the route configuration but not the router service

The RouterModule.forChild() method is needed when your application has multiple routing modules.

Remember that the router service takes care of synchronization between our application state and the browser URL. Instantiating multiple router services that interact with the same browser URL would lead to issues, so it is essential that there is only one instance of the router service in our application, no matter how many routing modules we import in our application.

When we import a routing module that is created using RouterModule.forRoot(), Angular will instantiate the router service. When we import a routing module that is created using RouterModule.forChild(), Angular will not instantiate the router service.

Therefore we can only use RouterModule.forRoot() once and use RouterModule.forChild() multiple times for additional routing modules.

Because our application only has one routing module, we use RouterModule.forRoot():

imports: [RouterModule.forRoot(routes)]

In addition, we also specify RouterModule in the exports property:

exports: [RouterModule]

This ensures that we don’t have to explicitly import RouterModule again in AppModule when AppModule imports AppRoutingModule.

Now that we have our AppRoutingModule, we need to import it in our AppModule to enable it.

Importing the routing configuration

To import our routing configuration into our application, we must import AppRoutingModule into our main AppModule.

Let’s open up src/app/app.module.ts and add AppRoutingModule to the imports array in AppModule‘s @NgModule metadata:

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 { TodoListComponent } from './todo-list/todo-list.component';
import { TodoListFooterComponent } from './todo-list-footer/todo-list-footer.component';
import { TodoListHeaderComponent } from './todo-list-header/todo-list-header.component';
import { TodoDataService } from './todo-data.service';
import { TodoListItemComponent } from './todo-list-item/todo-list-item.component';
import { ApiService } from './api.service';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [
    AppComponent,
    TodoListComponent,
    TodoListFooterComponent,
    TodoListHeaderComponent,
    TodoListItemComponent
  ],
  imports: [
    AppRoutingModule,
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [TodoDataService, ApiService],
  bootstrap: [AppComponent]
})
export class AppModule {
}

Because AppRoutingModule has RoutingModule listed in its exports property, Angular will import RoutingModule automatically when we import AppRoutingModule, so we don’t have to explicitly import RouterModule again (although doing so would not cause any harm).

Before we can try out our changes in the browser, we need to complete the third and final step.

Adding a router outlet

Although our application now has a routing configuration, we still need to tell Angular router where it can place the instantiated components in the DOM.

When our application is bootstrapped, Angular instantiates AppComponent because AppComponent is listed in the bootstrap property of AppModule:

@NgModule({
  // ...
  bootstrap: [AppComponent]
})
export class AppModule {
}

To tell Angular router where it can place components, we must add the <router-outlet></router-outlet> element to AppComponent‘s HTML template.

The <router-outlet></router-outlet> element tells Angular router where it can instantiate components in the DOM.

If you are familiar AngularJS 1.x router and UI-Router, you can consider <router-outlet></router-outlet> the Angular alternative to ng-view and ui-view.

Without a <router-outlet></router-outlet> element, Angular router would not know where to place the components and only AppComponent‘s own HTML would be rendered.

AppComponent currently displays a list of todo’s.

But instead of letting AppComponent display a list of todo’s, we now want AppComponent to contain a <router-outlet></router-outlet> and tell Angular router to instantiate another component inside AppComponent to display the list of todo’s.

To accomplish that, let’s generate a new component TodosComponent using Angular CLI:

$ ng generate component Todos

and move all HTML from src/app/app.component.html to src/app/todos/todos.component.html:

<!-- src/app/todos/todos.component.html -->
<section class="todoapp">
  <app-todo-list-header
    (add)="onAddTodo($event)"
  ></app-todo-list-header>
  <app-todo-list
    [todos]="todos"
    (toggleComplete)="onToggleTodoComplete($event)"
    (remove)="onRemoveTodo($event)"
  ></app-todo-list>
  <app-todo-list-footer
    [todos]="todos"
  ></app-todo-list-footer>
</section>

and all logic from src/app/app.component.ts to src/app/todos/todos.component.ts:

/* src/app/todos/todos.component.ts */
import { Component, OnInit } from '@angular/core';
import { TodoDataService } from '../todo-data.service';
import { Todo } from '../todo';

@Component({
  selector: 'app-todos',
  templateUrl: './todos.component.html',
  styleUrls: ['./todos.component.css'],
  providers: [TodoDataService]
})
export class TodosComponent implements OnInit {

  todos: Todo[] = [];

  constructor(
    private todoDataService: TodoDataService
  ) {
  }

  public ngOnInit() {
    this.todoDataService
      .getAllTodos()
      .subscribe(
        (todos) => {
          this.todos = todos;
        }
      );
  }

  onAddTodo(todo) {
    this.todoDataService
      .addTodo(todo)
      .subscribe(
        (newTodo) => {
          this.todos = this.todos.concat(newTodo);
        }
      );
  }

  onToggleTodoComplete(todo) {
    this.todoDataService
      .toggleTodoComplete(todo)
      .subscribe(
        (updatedTodo) => {
          todo = updatedTodo;
        }
      );
  }

  onRemoveTodo(todo) {
    this.todoDataService
      .deleteTodoById(todo.id)
      .subscribe(
        (_) => {
          this.todos = this.todos.filter((t) => t.id !== todo.id);
        }
      );
  }
}

Now we can replace AppComponent‘s template in src/app/app.component.html with:

<router-outlet></router-outlet>

and remove all obsolete code from AppComponent‘s class in src/app/app.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {

}

Finally, we update our todos route in src/app/app-routing.module.ts to instantiate TodosComponent instead of AppComponent:

const routes: Routes = [
  {
    path: '',
    redirectTo: 'todos',
    pathMatch: 'full'
  },
  {
    path: 'todos',
    component: TodosComponent
  }
];

Now, when our application is bootstrapped, Angular instantiates AppComponent and finds a <router-outlet></router-outlet> where Angular router can instantiate and activate components.

Let’s try out our changes in the browser.

Start your development server and your backend API by running:

$ ng serve
$ npm run json-server

and navigate your browser to http://localhost:4200.

Angular router reads the router configuration and automatically redirects our browser to http://localhost:4200/todos.

If you inspect the elements on the page, you will see that the TodosComponent is not rendered inside <router-outlet></router-outlet>, but right next to it:

<app-root>

    <!-- Angular router finds router outlet -->
    <router-outlet></router-outlet>

    <!-- and places the component right next to it, NOT inside it -->
    <app-todos></app-todos>
</app-root>

Our application now has routing enabled. Awesome!

Adding a wildcard route

When you navigate your browser to http://localhost:4200/unmatched-url, and you open up your browser’s developer tools, you will notice that Angular router logs the following error to the console:

Error: Cannot match any routes. URL Segment: 'unmatched-url'

To handle unmatched URL’s gracefully we need to do two things:

  1. Create PageNotFoundComponent (you can name it differently if you like) to display a friendly message that the requested page could not be found
  2. Tell Angular router to show the PageNotFoundComponent when no route matches the requested URL

Let’s start by generating PageNotFoundComponent using Angular CLI:

$ ng generate component PageNotFound

and edit its template in src/app/page-not-found/page-not-found.component.html:

<p>We are sorry, the requested page could not be found.</p>

Next, we add a wildcard route using ** as a path:

const routes: Routes = [
  {
    path: '',
    redirectTo: 'todos',
    pathMatch: 'full'
  },
  {
    path: 'todos',
    component: AppComponent
  },
  {
    path: '**',
    component: PageNotFoundComponent
  }
];

The ** matches any URL, including child paths.

Now, if you navigate your browser to http://localhost:4200/unmatched-url, PageNotFoundComponent is displayed.

Notice that the wildcard route must be the last route in our routing configuration for it to work as expected.

When Angular router matches a request URL to the router configuration, it stops processing as soon as it finds the first match.

So if we were to change the order of the routes to:

const routes: Routes = [
  {
    path: '',
    redirectTo: 'todos',
    pathMatch: 'full'
  },
  {
    path: '**',
    component: PageNotFoundComponent
  },
  {
    path: 'todos',
    component: AppComponent
  }
];

then todos would never be reached and PageNotFoundComponent would be displayed because the wildcard route would be matched first.

We have already done a lot, so let’s quickly recap what we have accomplished so far:

  • we set up Angular router
  • we created the routing configuration for our application
  • we refactored AppComponent to TodosComponent
  • we added <router-outlet></router-outlet> to AppComponent‘s template
  • we added a wildcard route to handle unmatched URLs gracefully

Next, we will create a resolver to fetch the existing todo’s from our backend API using Angular router.

Resolving Data using Angular router

In the part 3 of this series we already learned how to fetch data from our backend API using the Angular HTTP service.

Currently, when we navigate our browser to the todos URL, the following happens:

  1. Angular router matches the todos URL
  2. Angular router activates the TodosComponent
  3. Angular router places the TodosComponent next to <router-outlet></router-outlet> in the DOM
  4. The TodosComponent is displayed in the browser with an empty array of todo’s
  5. The todo’s are fetched from the API in the ngOnInit handler of theTodosComponent
  6. The TodosComponent is updated in the browser with the todo’s fetched from the API

If loading the todo’s in step 5 takes 3 seconds, the user will be presented with an empty todo list for 3 seconds before the actual todo’s are displayed in step 6.

If the TodosComponent were to have the following HTML in its template:

<div *ngIf="!todos.length">
  You currently do not have any todo's yet.
</div>

then the user would see this message for 3 seconds before the actual todo’s are displayed, which could totally mislead the user and cause the user to navigate away before the actual data comes in.

We could add a loader to TodosComponent that shows a spinner while the data is being loaded, but sometimes we may not have control over the actual component, for example when we use a third party component.

To fix this unwanted behavior, we need the following to happen:

  1. Angular router matches the todos URL
  2. Angular router fetches the todo’s from the API
  3. Angular router activates the TodosComponent
  4. Angular router places the TodosComponent next to <router-outlet></router-outlet> in the DOM
  5. The TodosComponent is displayed in the browser with the todo’s fetched from the API

where the TodosComponent is not displayed until the data from our API backend is available.

That is exactly what a resolver can do for us.

To let Angular router resolve the todo’s before it activates the TodosComponent, we must do two things:

  1. create a TodosResolver that fetches the todo’s from the API
  2. tell Angular router to use the TodosResolver to fetch the todo’s when activating the TodosComponent in the todos route

By attaching a resolver to the todos route we ask Angular router to resolve the data first, before TodosComponent is activated.

So let’s create a resolver to fetch our todo’s.

Creating TodosResolver

Angular CLI does not have a command to generate a resolver, so let’s create a new file src/todos.resolver.ts manually and add the following code:

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Todo } from './todo';
import { TodoDataService } from './todo-data.service';

@Injectable()
export class TodosResolver implements Resolve<Observable<Todo[]>> {

  constructor(
    private todoDataService: TodoDataService
  ) {
  }

  public resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<Todo[]> {
    return this.todoDataService.getAllTodos();
  }
}

We define the resolver as a class that implements the Resolve interface.

The Resolve interface is optional, but lets our TypeScript IDE or compiler ensure that we implement the class correctly by requiring us to implement a resolve() method.

When Angular router needs to resolve data using a resolver, it calls the resolver’s resolve() method and expects the resolve() method to return a value, a promise, or an observable.

If the resolve() method returns a promise or an observable Angular router will wait for the promise or observable to complete before it activates the route’s component.

When calling the resolve() method, Angular router conveniently passes in the activated route snapshot and the router state snapshot to provide us with access to data (such as route parameters or query parameters) we may need to resolve the data.

The code for TodosResolver is very concise because we already have a TodoDataService that handles all communication with our API backend.

We inject TodoDataService in the constructor and use its getAllTodos() method to fetch all todo’s in the resolve() method.

The resolve method returns an observable of the type Todo[], so Angular router will wait for the observable to complete before the route’s component is activated.

Now that we have our resolver, let’s configure Angular router to use it.

Resolving todo’s via the router

To make Angular router use a resolver, we must attach it to a route in our route configuration.

Let’s open up src/app-routing.module.ts and add our TodosResolver to the todos route:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { TodosComponent } from './todos/todos.component';
import { TodosResolver } from './todos.resolver';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'todos',
    pathMatch: 'full'
  },
  {
    path: 'todos',
    component: TodosComponent,
    resolve: {
      todos: TodosResolver
    }
  },
  {
    path: '**',
    component: PageNotFoundComponent
  }
];

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

We import TodosResolver:

import { TodosResolver } from './todos.resolver';

and add it as a resolver the the todos route:

{
  path: 'todos',
  component: TodosComponent,
  resolve: {
    todos: TodosResolver
  }
}

This tells Angular router to resolve data using TodosResolver and assign the resolver’s return value as todos in the route’s data.

A route’s data can be accessed from the ActivatedRoute or ActivatedRouteSnapshot, which we will see in the next section.

You can add static data directly to a route’s data using the data property of the route:

{
  path: 'todos',
  component: TodosComponent,
  data: {
    title: 'Example of static route data'
  }
}

or dynamic data using a resolver specified in the the resolve property of the route:

resolve: {
  path: 'todos',
  component: TodosComponent,
  resolve: {
    todos: TodosResolver
  }
}

or both at the same time:

resolve: {
  path: 'todos',
  component: TodosComponent,
  data: {
    title: 'Example of static route data'
  }
  resolve: {
    todos: TodosResolver
  }
}

As soon as the resolvers from the resolve property are resolved, their values are merged with the static data from the data property and all data is made available as the route’s data.

Angular router uses Angular dependency injection to access resolvers, so we have to make sure we register TodosResolver with Angular’s dependency injection system by adding it to the providers property in AppRoutingModule‘s @NgModule metadata:

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

When you navigate your browser to http://localhost:4200, Angular router now:

  1. redirects the URL from / to /todos
  2. sees that the todos route has TodosResolver defined in its resolve property
  3. run the resolve() method from TodosResolver, waits for the result and assigns the result to todos in the route’s data
  4. activates TodosComponent

If you open up the network tab of your developer tools, you will see that the todo’s are now fetched twice from the API. Once by Angular router and once by the ngOnInit handler in TodosComponent.

So Angular router already fetches the todo’s from the API, but TodosComponent still uses its own internal logic to load the todo’s.

In the next section, we will update TodosComponent to use the data resolved by Angular router.

Using resolved data

Let’s open up app/src/todos/todos.component.ts.

The ngOnInit() handler currently fetches the todo’s directly from the API:

public ngOnInit() {
  this.todoDataService
    .getAllTodos()
    .subscribe(
      (todos) => {
        this.todos = todos;
      }
    );
}

Now that Angular router fetches the todo’s using TodosResolver, we want to fetch the todo’s in TodosComponent from the route data instead of the API.

To access the route data, we must import ActivatedRoute from @angular/router:

import { ActivatedRoute } from '@angular/router';

and use Angular dependency injection to get a handle of the activated route:

constructor(
  private todoDataService: TodoDataService,
  private route: ActivatedRoute
) {
}

Finally, we update the ngOnInit() handler to get the todo’s from the route data instead of the API:

public ngOnInit() {
  this.route.data
    .map((data) => data['todos'])
    .subscribe(
      (todos) => {
        this.todos = todos;
      }
    );
}

The ActivatedRoute exposes the route data as an observable, so our code barely changes.

We replace this.todoDataService.getAllTodos() with this.route.data.map((data) => data['todos']) and all the rest of the code remains unchanged.

If you navigate your browser to localhost:4200 and open up the network tab, you will no longer see two HTTP requests fetching the todo’s from the API.

Mission accomplished! We have successfully integrated Angular router in our application!

Before we wrap up, let’s run our unit tests:

ng serve

1 unit tests fails:

Executed 11 of 11 (1 FAILED) 
TodosComponent should create FAILED
    'app-todo-list-header' is not a known element

When TodosComponent is tested, the testbed is not aware of TodoListHeaderComponent and thus Angular complains that it does not know the app-todo-list-header element.

To fix this error, let’s open up app/src/todos/todos.component.spec.ts and add NO_ERRORS_SCHEMA to the TestBed options:

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [TodosComponent],
    schemas: [
      NO_ERRORS_SCHEMA
    ]
  })
    .compileComponents();
}));

Now Karma shows another error:

Executed 11 of 11 (1 FAILED) 
TodosComponent should create FAILED
    No provider for ApiService!

Let’s add the necessary providers to the test bed options:

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [TodosComponent],
    schemas: [
      NO_ERRORS_SCHEMA
    ],
    providers: [
      TodoDataService,
      {
        provide: ApiService,
        useClass: ApiMockService
      }
    ],
  })
    .compileComponents();
}));

which again raises another error:

Executed 11 of 11 (1 FAILED) 
TodosComponent should create FAILED
    No provider for ActivatedRoute!!

Let’s add one more provider for ActivatedRoute to the testbed options:

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [TodosComponent],
    schemas: [
      NO_ERRORS_SCHEMA
    ],
    providers: [
      TodoDataService,
      {
        provide: ApiService,
        useClass: ApiMockService
      },
      {
        provide: ActivatedRoute,
        useValue: {
          data: Observable.of({
            todos: []
          })
        }
      }
    ],
  })
    .compileComponents();
}));

We assign the provider for ActivatedRoute a mock object that contains an observable data property to expose a test value for todos.

Now the unit tests successfully pass:

Executed 11 of 11 SUCCESS 

Fabulous! To deploy our application to a production environment, we can now run:

ng build --aot --environment prod

and upload the generated dist directory to our hosting server. How sweet is that?

We covered a lot in this article, so let’s recap what we have learned.

Summary

In the first article, we learned how to:

  • initialize our Todo application using Angular CLI
  • create a Todo class to represent individual todo’s
  • create a TodoDataService service to create, update and remove todo’s
  • use the AppComponent component to display the user interface
  • deploy our application to GitHub pages

In the second article, we refactored AppComponent to delegate most of its work to:

  • a TodoListComponent to display a list of todo’s
  • a TodoListItemComponent to display a single todo
  • a TodoListHeaderComponent to create a new todo
  • a TodoListFooterComponent to show how many todo’s are left

In the third article, we learned how to:

  • create a mock REST API backend
  • store the API URL as an environment variable
  • create an ApiService to communicate with the REST API
  • update the TodoDataService to use the new ApiService
  • update the AppComponent to handle asynchronous API calls
  • create an ApiMockService to avoid real HTTP calls when running unit tests

In this fourth article, we learned:

  • why an application may need routing
  • what a JavaScript router is
  • what Angular router is, how it works and what it can do for you
  • how to set up Angular router and configure routes for our application
  • how to tell Angular router where to place components in the DOM
  • how to gracefully handle unknown URLs
  • how to use a resolver to let Angular router resolve data

All code from this article is available at https://github.com/sitepoint-editors/angular-todo-app/tree/part-4.

In part five, we will implement authentication to prevent unauthorized access to our application.

Recommended Courses

Todd Motto
Expert-led online AngularJS, Angular and TypeScript training courses for individuals and teams. Use coupon code 'SITEPOINT' at checkout to get 25% off.

So stay tuned for more and, as always, feel free to leave your thoughts and questions in the comments!

Sponsors