How to Handle Routing in Angular Single Page Applications (SPAs) with JavaScript and Node.js

May 02, 2018
Written by
Maciej Treder
Contributor
Opinions expressed by Twilio contributors are their own

Angular routing

Routing is an essential concept in Single Page Applications (SPA). When your application is divided into separated logical sections, and all of them are under their own URL, your users can easily share links among each other.

In my previous post, Building an App from Scratch with Angular and Webpack, I covered how to start working with Angular and Webpack. Today we will take a closer look at implementing more components and navigating between them.

In this post we will:

  • Navigate between components.
  • Use path and query parameters.
  • Prefetch data with resolvers.
  • Create lazy loaded modules.

To accomplish these tasks, make sure you:

Creating Components

As a codebase, we will use the code from the previous Angular and Webpack blog post. Clone it and install the dependencies by running:

 

git clone -b angular_and_webpack_demystified_step3 https://github.com/maciejtreder/angular-universal-pwa.git myAngularApp
cd myAngularApp
npm install

Right now our application is fairly limited with just one page. Let’s add more components to it:

mkdir src/app/home
touch src/app/home/home.component.ts
mkdir src/app/posts
touch src/app/posts/list.component.ts
touch src/app/posts/list.component.html
touch src/app/menu.component.ts
mkdir src/app/model
touch src/app/model/ipost.ts

Now we need also install @angular/router which we will be playing around with today.

npm install --save @angular/router

 

Okay, we have a lot of new files. We should walk through them and modify them. We will start by modifying an entry point of the application – src/app/app.component.ts:

 

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

@Component({
 selector: 'my-app',
 template: '<menu></menu><router-outlet></router-outlet>',
})
export class AppComponent {

 constructor(){
 }
}

 

Next, we need to create one of the new components –  the HomeComponent. It will be displayed as a “welcome message” when our user navigates to the main page of our app. Place the following code into src/app/home/home.component.ts:

 

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

@Component({
   template: '<h1>Hello World!</h1>'
})
export class HomeComponent {
}

 

Now we will create a second component – ListComponent. Here, the user will be able to see a list of posts from https://jsonplaceholder.typicode.com/posts. Update src/app/posts/list.component.ts to contain:

 

import { Component, OnInit } from '@angular/core';
import { EchoService } from '../services/echo.service';
import { Observable } from 'rxjs/Observable';
import { IPost } from '../model/ipost';

@Component({
   templateUrl: `list.component.html`
})
export class ListComponent implements OnInit {
   public posts: Observable<IPost[]>;

   constructor(private echoService: EchoService) {}

   public ngOnInit(): void {
       this.posts = this.echoService.getPosts();
   }
}

 

Place the following code into src/app/posts/list.component.html:

 

<h1>Posts list</h1>
<ul>
<li *ngFor="let post of posts | async">
		{{post.id}} - {{post.title}}
</li>
</ul>

 

Now we need to create a method used in ListComponentechoService.getPosts();. This method will be returning an IPost type, so let’s create it first. Place this code in src/app/model/ipost.ts:

 

export interface IPost {
	id: number;
	title: string;
body: string;
}

 

And this code in src/app/services/echo.service.ts:

 


import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { IPost } from '../model/ipost';

@Injectable()
export class EchoService {
	constructor(private httpClient: HttpClient) {}

	public getPosts(): Observable<IPost[]> {
		return this.httpClient.get<IPost[]>('https://jsonplaceholder.typicode.com/posts');
	}
}

 

Add Routing

We are ready to tell Angular how the user will be navigating within our application. To do that we are registering RouterModule.forRoot() in the AppModule. Copy this code to src/app/app.module.ts:

 


import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { ListComponent } from './posts/list.component';
import { BrowserModule } from '@angular/platform-browser';
import { MenuComponent } from './menu.component';
import { RouterModule } from '@angular/router';
import { EchoService } from './services/echo.service';
import { HttpClientModule } from '@angular/common/http';
 
@NgModule({
   bootstrap: [ AppComponent ],
 imports: [
     BrowserModule,
     HttpClientModule,
     RouterModule.forRoot([
         { path: '', redirectTo: '/home', pathMatch: 'full' },
       { path: 'home', component: HomeComponent },
       { path: 'posts', component: ListComponent }
     ])
 ],
 declarations: [ AppComponent, MenuComponent, ListComponent, HomeComponent],
   providers: [EchoService]
})
export class AppModule {
}

 

Apart from routes, we also declared a MenuComponent used inside AppComponent, just before . Go on and create it by placing this code in src/app/menu.component.ts:

 

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

@Component({
  selector: 'menu',
  template: `
      <ul>
          <li><a routerLink="home" routerLinkActive="active">Home</a></li>
          <li><a routerLink="posts" routerLinkActive="active">Posts list</a></li>
      </ul>
  `,
  styles: [`
      :host {margin: 0; padding: 0}
      ul {list-style-type: none; padding: 0;}
      li {display: inline-block;}
      a {border: 1px solid #666666; background: #aaaaaa; border-radius: 5px; box-shadow: 1px 1px 5px black; color: white; font-weight: bold; padding: 5px; text-decoration: none}
      a.active {text-decoration: underline; color: darkslategray;}
      li + li a {margin-left: 20px;}
  `]
})
export class MenuComponent {
}

 

If you take a closer look, you will see the connection between ‘routerLink’ and routes declared in the AppModule. This is how we are linking stuff in Angular; routerLink is an Angular built-in directive which takes ‘path’ as a parameter and matches it with ‘path’ declared in RouterModule. When there is a match, it loads given component into , which we added earlier into src/app/app.component.ts, next to the

:

 

 

 template: '<menu></menu><router-outlet></router-outlet>',

We have also introduced one more Angular directive, routerLinkActive. By adding it, we are telling Angular that whenever a given route is activated, we want to add to the <a> tag a given class (in our case active).

Now we are ready to compile and run our app:

npm run build
npm start

 

Open your browser and navigate to http://localhost:3000. This is what you should see in your browser:

Click on the “Posts list” link at the top and the page should change respectively:

You’ll find all the code up to this point in this GitHub repository which you can clone:

 

git clone -b angular_routing_step1 https://github.com/maciejtreder/angular-universal-pwa.git myAngularApp
cd myAngularApp/
npm install

 

Path Params

So far, you know how to navigate between components. For some pages we might want to make Angular pass some values via the URL. Here path parameters comes into use.

Before we start writing code, we need to create a file in which we will place a new component:

 

touch src/app/posts/post.component.ts

 

First, we are going to prepare a route with a placeholder for the value which we want to pass in the routes definition. Add the following route and component import to src/app/app.module.ts:

 


import { NgModule } from '@angular/core';
import { PostComponent } from './posts/post.component';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { ListComponent } from './posts/list.component';
import { BrowserModule } from '@angular/platform-browser';
import { MenuComponent } from './menu.component';
import { RouterModule } from '@angular/router';
import { EchoService } from './services/echo.service';
import { HttpClientModule } from '@angular/common/http';
 
@NgModule({
   bootstrap: [ AppComponent ],
 imports: [
     BrowserModule,
     HttpClientModule,
     RouterModule.forRoot([
        { path: '', redirectTo: '/home', pathMatch: 'full' },
        { path: 'home', component: HomeComponent },
        { path: 'posts', component: ListComponent },
        { path: 'posts/:id', component: PostComponent },
     ])
 ],
 declarations: [ AppComponent, MenuComponent, ListComponent, HomeComponent, PostComponent],
   providers: [EchoService]
})
export class AppModule {
}

 

Now you need to create the PostComponent. Place this code in the src/app/posts/post.component.ts file:

 

import { Observable } from 'rxjs';
import { IPost } from '../model/ipost';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EchoService } from '../services/echo.service';

@Component({
   template: '<h1>{{(post | async)?.title}}</h1><p>{{(post | async)?.body}}</p>'
})
export class PostComponent implements OnInit {
   public post: Observable<IPost>;

   constructor(private route: ActivatedRoute, private echoService: EchoService){}

   ngOnInit() {
       this.post = this.echoService.getPost(this.route.snapshot.params['id']);
   }
}

 

We are injecting the ActivatedRoute service in the constructor; it is representing the actual routing state of our application. With the use of this service, we can retrieve information about the active path, outlets, or pathParams. In our case, we are retrieving them by calling this.route.snapshot.params[‘id’]. Note that the index name which we are calling on this.route.snapshot.params is exactly the same as the one we used for the placeholder in app.module.ts. After retrieving the post ID from the URI, we are passing it to the echoService.getPost(id: number) method to retrieve the given post.

Let’s implement this method! Add the following code to the src/app/services/echo.service.ts file:

 


constructor(private httpClient: HttpClient) {}

public getPost(id: number): Observable<IPost> {
    return this.httpClient.get<IPost>('https://jsonplaceholder.typicode.com/posts/' + id);
}

 

And check if the routing works as expected:

 

npm run build
npm start

 

Here is what you should see after navigating to http://localhost:3000/posts/1

The final step for this section is linking posts in the ListComponent to the PostComponent. Modify src/app/posts/list.component.html as follows:

 


<h1>Posts list</h1>
<ul>
  <li *ngFor="let post of posts | async">
  <a [routerLink]="['/posts', post.id]">
    {{post.title}}
  </a>
</li>
</ul>

 

We are passing an array to the routerLink directive. As a first element, we are telling Angular which route we want to activate; the second one is a value which we want to pass as a pathParam. Now every post title listed in the ListComponent routes to the PostComponent and passes the id of given post. Then PostComponent retrieves it from the back end and displays it.

Congratulations! Now you know how to use path parameters in Angular!

Query Params

We are displaying 100 posts in the ListComponent. That’s a lot. We should make it more readable by adding pagination.

First, replace the current code in src/app/posts/list.component.ts with the following:

 

import { Component, OnInit } from '@angular/core';
import { EchoService } from '../services/echo.service';
import { Observable } from 'rxjs/Observable';
import { IPost } from '../model/ipost';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
   templateUrl: `list.component.html`,
   styles: [`
      a:hover {cursor: pointer}
  `]
})
export class ListComponent implements OnInit {
   public posts: IPost[];
   public page: number;

   private allPosts: Observable<IPost[]>;

   constructor(private echoService: EchoService, private activatedRoute: ActivatedRoute, private router: Router) {}

   public ngOnInit(): void {
       this.allPosts = this.echoService.getPosts();
       this.allPosts.subscribe(posts => {
           this.activatedRoute.queryParams.subscribe(params => {
               this.page = +params['page'] || 0;
               this.posts = posts.filter(post => post.id > this.page * 10 && post.id < this.page * 10 + 11);
           })
       });
   }

   public nextPage(): void {
       this.router.navigate(['posts'], {queryParams: {page: this.page + 1}});
   }

   public previousPage(): void {
       this.router.navigate(['posts'], {queryParams: {page: this.page - 1}});
   }
}

 

What we are doing here is displaying a filtered part of the allPosts array, based on the query parameter page, retrieved from this.activatedRoute.queryParams observable.

The last step is changing the template. Copy this code to src/app/posts/list.component.html:

 


<h1>Posts list</h1>
<a (click)="previousPage()" *ngIf="page > 0">previous</a>
<a (click)="nextPage()" *ngIf="page < 9">next</a>
<ul>
 <li *ngFor="let post of posts">
    <a [routerLink]="['/posts', post.id]">
       {{post.title}}
    </a>
 </li>
</ul>

 

Let’s check if pagination works. Re-run:

 

npm run build
npm start

 

Go back to the List page at: http://localhost:3000/posts and click on the “previous” and “next” links. The content of the page should update. You should also see the URL update with query parameter called page.

You’ll find all the code up to this point in this GitHub repository which you can clone:

 

git clone -b angular_routing_step2 https://github.com/maciejtreder/angular-universal-pwa.git myAngularApp
cd myAngularApp/
npm install

 

Prefetching Data with Resolvers

There is one more thing which is not looking nice in our app. Did you notice the delay between loading component and retrieving data displayed in the template? It occurs because the flow of retrieving data in our applications is:

User clicks on the link -> Angular navigates to the route and loads component -> component retrieves data from back end -> data is displayed to the user.

Wouldn’t it be nice to first retrieve data and then load component when the data is already fetched and ready to display? That’s job for Resolver. Let’s play with it a little bit.

Create the necessary files:

 

mkdir src/app/services/resolvers
touch src/app/services/resolvers/posts.resolver.ts
touch src/app/services/resolvers/post.resolver.ts

 

We will start by creating the PostsResolver. Place this code in src/app/services/resolvers/posts.resolver.ts:

 

import { Injectable }from '@angular/core';
import { Observable }from 'rxjs/Observable';
import { Resolve, RouterStateSnapshot,
   ActivatedRouteSnapshot } from '@angular/router';

import { IPost } from '../../model/ipost';
import { EchoService } from '../echo.service';

@Injectable()
export class PostsResolver implements Resolve<IPost[]> {
   constructor(private echoService: EchoService) {}

   resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IPost[]> {
       return this.echoService.getPosts();
   }
}

 

As you can see we moved the retrieving posts list logic from ListComponent into this file. Now you can edit the ListComponent to get this data from the router. Make the following changes in src/app/posts/list.component.ts:

 


import { Component, OnInit } from '@angular/core';
import { IPost } from '../model/ipost';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  templateUrl: `list.component.html`,
   styles: [`
       a:hover {cursor: pointer}
   `]
})
export class ListComponent implements OnInit {
   public posts: IPost[];
  public page: number;

  constructor(private activatedRoute: ActivatedRoute, private router: Router) {}

  public ngOnInit(): void {
      this.activatedRoute.queryParams.subscribe(params => {
          this.page = +params['page'] || 0;
          this.posts = this.activatedRoute.snapshot.data.posts.filter(post => post.id > this.page * 10 && post.id < this.page * 10 + 11);
      });
  }

  public nextPage(): void {
      this.router.navigate(['posts'], {queryParams: {page: this.page + 1}});
  }

  public previousPage(): void {
      this.router.navigate(['posts'], {queryParams: {page: this.page - 1}});
  }
}

 

The last step is to inform Angular Router that we are using Resolver to prefetch data. We will do that in the AppModule. Edit it accordingly (src/app/app.module.ts):

 


import { NgModule } from '@angular/core';
import { PostComponent } from './posts/post.component';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { ListComponent } from './posts/list.component';
import { BrowserModule } from '@angular/platform-browser';
import { MenuComponent } from './menu.component';
import { RouterModule } from '@angular/router';
import { EchoService } from './services/echo.service';
import { PostsResolver } from './services/resolvers/posts.resolver';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
	bootstrap: [ AppComponent ],
	imports: [
		BrowserModule,
		HttpClientModule,
		RouterModule.forRoot([
			{ path: '', redirectTo: '/home', pathMatch: 'full' },
			{ path: 'home', component: HomeComponent },
			{ path: 'posts', component: ListComponent, resolve: {posts: PostsResolver} },
			{ path: 'posts/:id', component: PostComponent },
		])
	],
	declarations: [ AppComponent, MenuComponent, ListComponent, HomeComponent, PostComponent],
	providers: [EchoService, PostsResolver]
})
export class AppModule {
}

 

Now we can bundle the app again, and check if we still see the delay between loading component and retrieving data displayed in it:

 

npm run build
npm start

 

Navigate to http://localhost:3000 and click on posts list link. And..? Data is prefetched before Angular navigates to the component.

Error Handling

We will use a similar concept to retrieve data for PostComponent. The only additional logic we will add here is error handling, in case that post won’t be found in the back end.
Copy this code to src/app/services/resolvers/post.resolver.ts:

 


import 'rxjs/Rx';
import { Injectable }from '@angular/core';
import { Observable }from 'rxjs/Observable';
import { Resolve, RouterStateSnapshot,
   ActivatedRouteSnapshot } from '@angular/router';

import { IPost } from '../../model/ipost';
import { EchoService } from '../echo.service';

@Injectable()
export class PostResolver implements Resolve<IPost> {
   constructor(private echoService: EchoService) {}

   resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IPost> {
       return this.echoService.getPost(+route.paramMap.get('id')).catch(() => Observable.of({title: 'Post not found', body: 'Post with given id doesn\'t exist'} as IPost));
   }
}

 

Now we should change PostComponent accordingly (src/app/posts/post.component.ts):

 


import { IPost } from '../model/ipost';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
   template: '<h1>{{post.title}}</h1><p>{{post.body}}</p>'
})
export class PostComponent implements OnInit {
   public post: IPost;

   constructor(private route: ActivatedRoute){}

   ngOnInit() {
       this.post = this.route.snapshot.data.post;
   }
}

 

And routing in src/app/app.module.ts:

 


import { NgModule } from '@angular/core';
import { PostComponent } from './posts/post.component';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { ListComponent } from './posts/list.component';
import { BrowserModule } from '@angular/platform-browser';
import { MenuComponent } from './menu.component';
import { RouterModule } from '@angular/router';
import { EchoService } from './services/echo.service';
import { PostsResolver } from './services/resolvers/posts.resolver';
import { HttpClientModule } from '@angular/common/http';
import { PostResolver } from './services/resolvers/post.resolver';

@NgModule({
   bootstrap: [ AppComponent ],
   imports: [
       BrowserModule,
       HttpClientModule,
       RouterModule.forRoot([
           { path: '', redirectTo: '/home', pathMatch: 'full' },
           { path: 'home', component: HomeComponent },
           { path: 'posts', component: ListComponent, resolve: {posts: PostsResolver} },
           { path: 'posts/:id', component: PostComponent, resolve: {post: PostResolver} },
       ])
   ],
   declarations: [ AppComponent, MenuComponent, ListComponent, HomeComponent, PostComponent],
   providers: [EchoService, PostsResolver, PostResolver]
})
export class AppModule {
}

 

Now PostComponent uses prefetched data as well. Let’s check if error handling works appropriately.

 

npm run build
npm start

 

Navigate to http://localhost:3000/posts/101. Here is what you should see:

You’ll find all the code up to this point in this GitHub repository you can clone:

 

git clone -b angular_routing_step3 https://github.com/maciejtreder/angular-universal-pwa.git myAngularApp
cd myAngularApp/
npm install

 

Lazy Loading

We have our app complete and running. We are prefetching data to improve the user experience. Can we do even more? Yes! Right now our app is bundled into file dist/app.js, and this file is loaded in the user browser even if he only reaches the main page. We can enforce Angular Compiler and Webpack to split it into more files, and load them on demand when the user navigates to different parts of the app. This concept is called Lazy Loading.

First, create the file we are going to edit:

 

touch src/app/posts/posts.module.ts

 

Start by preparing our new module PostsModule. Place the following code in the src/app/posts/posts.module.ts file:

 

import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ListComponent } from './list.component';
import { PostComponent } from './post.component';
import { PostResolver } from '../services/resolvers/post.resolver';
import { PostsResolver } from '../services/resolvers/posts.resolver';
import { EchoService } from '../services/echo.service';
import { CommonModule } from '@angular/common';

@NgModule({
   declarations: [ListComponent, PostComponent],
   imports: [
       CommonModule,
       RouterModule.forChild([
           { path: '', component: ListComponent, resolve: {posts: PostsResolver} },
           { path: ':id', component: PostComponent, resolve: {post: PostResolver} }
       ])
   ],
   providers: [EchoService, PostsResolver, PostResolver]
})
export class PostsModule {}

 

What do we have here? We declared RouterModule.forChild(), in which we are specifying what routes should be served from this module. We also moved our resolvers here: PostResolver, PostsResolver, and EchoService used by resolvers to prefetch data. The last thing we do is declare components, ListComponent and PostComponent to which this module is navigating.

Now that we moved all of this code into a separate module, we do not require it any longer in the AppModule. Replace the code in the src/app/app.module.ts file with this code:

 

import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { BrowserModule } from '@angular/platform-browser';
import { MenuComponent } from './menu.component';
import { RouterModule } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  bootstrap: [ AppComponent ],
  imports: [
      BrowserModule,
      HttpClientModule,
      RouterModule.forRoot([
          { path: '', redirectTo: '/home', pathMatch: 'full' },
          { path: 'home', component: HomeComponent },
          { path: 'posts', loadChildren: './posts/posts.module#PostsModule'},
      ])
  ],
  declarations: [ AppComponent, MenuComponent, HomeComponent],
})
export class AppModule {
}

 

Build the app again:

 

npm run build
npm start

 

As you can see from the output, we have one more file created by the Angular compiler:

 

Hash: 38a5e7c390e0623c5b35
Version: webpack 3.10.0
Time: 6265ms
                 Asset       Size  Chunks                    Chunk Names
              0.app.js     759 kB       0  [emitted]  [big]  
                app.js    1.81 MB       1  [emitted]  [big]  main
assets/styles/main.css  364 bytes          [emitted]         
assets/img/sandbox.png     167 kB          [emitted]         
            index.html  563 bytes          [emitted]      

 

Let’s check how the app behaves now. First we navigate to http://localhost:3000 and take a look at the Network tab of Developer Tools:

We performed eight requests while loading HomeComponent. Now navigate to the Posts List:

As you can see, the rest of the application is loaded on demand, only when the user navigates to the given route.

Summary

Today, we learned how to create routing in the Angular app. We covered the basics as well as more advanced concepts like prefetching data using path resolvers, passing data with pathParams, and decreasing load time with lazy loading.

I hope that this post was helpful for you and you will start to use your new knowledge in your one-in-a-million app!

Take a look at the GitHub repository for step 4 and check out angular-universal-pwa to learn more Angular features.

Maciej Treder:
contact@maciejtreder.com
https://www.maciejtreder.com
@maciejtreder (GitHub, Twitter, StackOverflow, LinkedIn)