Angular Routing Part 2: Basic routing

In the previous post, we created the components for our book tracking application but we couldn’t navigate between the components. Now let’s start by adding some routing to the application.

RouterModule

Routing allows you to display content depending on the URL path. Angular has a module that can help you with that. To enable this, you need to import the RouterModule from the @angular/router package. You can import this multiple times in your application but keep in mind that you cannot have more than one router service active.

To import the RouterModule in our application, we create a module for the routing in the app folder and import it in app.module.ts.

ng generate module app-routing --flat
app.module.ts

import { AppRoutingModule } from './app-routing.module';
...
imports: [
  ...
  AppRoutingModule
],
...

In the app-routing module, we start declaring our routes.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { BooksComponent } from './books/books.component';
import { BookDetailComponent } from './books/book-detail/book-detail.component';
import { BookEditComponent } from './books/book-edit/book-edit.component';
import { PageNotFoundComponent } from './pages/page-not-found/page-not-found.component';

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent
  },
  {
    path: 'books',
    component: BooksComponent
  },
  {
    path: 'books/:id',
    component: BookDetailComponent
  },
  {
    path: 'books/:id/edit',
    component: BookEditComponent
  },
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
  },
  {
    path: '**',
    component: PageNotFoundComponent
  }
];

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

First, we add our route definitions to a constant ‘routes’. We give these route definitions to the RouterModule in the .forRoot directive. Note that we defined an empty route that redirects to the home-page and we defined a wildcard route (‘**’). Any route that does not match an earlier defined path in your configuration, will use this route. It is therefore very important that you place the wildcard route at the end of the array. If you placed it first, this route will always be the matching route.

Router-outlet

To let angular know where the components need to be rendered when you navigate through your application, you need to add the router-outlet selector. This is a directive that’s available in the @angular/router package and you can see this as a placeholder for where the routed components are rendered. We need to add it to app.component.html. Replace the app-home selector with this router-outlet.

<app-navbar></app-navbar>
<div class="container">
  <router-outlet></router-outlet>
</div>

When we now change the URL to https://localhost:4200/books, we see our book list. Yeah!

But when we want to go back to our homepage and click on home nothing happens. So now we can only navigate by changing the URL in the address bar. Hmm… that’s not very handy.

RouterLink

To make the links in the toolbar work, we can use the RouterLink directive to bind an HTML element to a route. When you then click on this HTML element, you navigate to the bound route.

Let’s add this routerLink to navbar.component.html. There are two ways you can use routerLink: specifying the URL as a string or as an array. When we use the first way, the resulting code will be the following.

<mat-toolbar color="primary">
    <mat-toolbar-row>
        <h1 class="title">{{title | uppercase }}</h1>
        <a *ngFor="let item of menuItems" routerLink="{{item.path}}">{{item.title}}</a>
    </mat-toolbar-row>
</mat-toolbar>

Using the second way, the anchor tag will be the following:

<a *ngFor="let item of menuItems" [routerLink]="[item.path]">{{item.title}}</a>

When we now click on a link in the toolbar, our application navigates to the right page. But… wouldn’t it be nice if you could see which link is active in the toolbar? You can easily achieve that by adding the routerLinkActive directive to the anchor-tag. This directive adds or removes classes from an HTML element that is bound to a RouterLink. So with this directive, we can toggle CSS classes based on the state of the route. So let’s add this to the anchor-tag and create the CSS class.

 <a *ngFor="let item of menuItems" routerLink="{{item.path}}" routerLinkActive="active">{{item.title}}</a> 

In navbar.component.scss we add the following CSS-class:

a.active {
    color: map-get($map: $mat-pink, $key: 500) !important;
    font-weight: bold;
}

And now we see which link is active.

But when you click on “Add new book”, we see two problems:

  • not only “Add new book” is highlighted but also the “Your book list”
  • There is already a book with information filled in.

The second problem happens because in the onInit-method in the book-edit component we always get the details from the book with id 1. We will fix this later.

The first problem is because “Add new book” is a child route. When a child route is active, all the parent routes are also marked as active and the routerLinkActive classes are also applied. Sometimes this is desired behaviour, but in this case, it is not. You can prevent this by adding routerLinkActiveOptions to the anchor-tag.

<mat-toolbar color="primary">
    <mat-toolbar-row>
        <h1 class="title">{{title | uppercase }}</h1>
        <a *ngFor="let item of menuItems" [routerLink]="[item.path]" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">{{item.title}}</a>
    </mat-toolbar-row>
</mat-toolbar>

Navigate and NavigateByUrl

Sometimes we need to trigger navigation via code. To navigate programmatically the @angular/router package has two methods: navigate and navigateByUrl.

To use these methods you need to inject the router service in your component.

constructor(private router: Router){ }

When you use the navigateByUrl method in your code, you pass the URL as a string (like in routerLink) and when you use the navigate method, you pass the URL as an array (like in [routerLink]).

this.router.navigateByUrl('home');
this.router.navigate(['home']);

Passing data to route

Our application is almost completely working, we only have the problem that when we click on “Add new book” there is already data in the form and the page title is better “Add book” instead of “Edit book” and we want to be able to edit a book from our book list because at the moment when we click on the edit-icon nothing happens. Also when we click on the book title in the book list, we want to navigate to the book-details component.

First, we are going to add navigation to the edit-icon. To navigate to the book-edit component, we need to navigate to path ‘books/:id/edit ‘ where :id is a parameter. So we need to add the book id to the route. Depending on which method you use for the navigation this can be done in the following ways:

<a routerLink="/books/{{book.id}}/edit"></a>
<a [routerLink]="['/books', book.id, 'edit']"></a>

Getting the passed data in the component

To get data from the route, we can use the ActivatedRoute interface from @angular/router. With this interface, we can access information about the route associated with the component loaded in the router-outlet. To use this interface, we need to inject it in the component. We then can get the book-id from the snapshot parameters. The onInit-method from the book-edit component needs to be changed to the following:

constructor(
    private route: ActivatedRoute,
    private bookService: BookService) { }

public ngOnInit(): void {
    const id = this.route.snapshot.paramMap.get('id');
    if (isNaN(+id) && id === "new") {
      this.book = {
        id: 0,
        title: '',
        authors: [],
        description: '',
        publishDate: new Date(),
        publisher: '',
        startReadingDate: null,
        readDate: null,
        rating: 0
      };
      this.authorRequired = true;
      this.pageTitle = "Add book";
    } else {
      this.bookService.getBook(+id)
        .subscribe(book => {
          this.book = book;
          this.book.authors.length !== 0 ? this.authorRequired = false : this.authorRequired = true;
          this.pageTitle = "Edit book";
        });
    }
  }

You see that we get the id from the ActivatedRoute. If the id is “new”, we create a new book-object and set the page title to “Add book”. When the id is a number, we get the book from the database.

For the book-detail component, we apply the same logic.

ngOnInit(): void {
    const id = this.route.snapshot.paramMap.get('id');
    if (isNaN(+id) && id === "new") {
      const book: Book = {
        id: 0,
        title: '',
        authors: [],
        description: '',
        publishDate: new Date(),
        publisher: '',
        startReadingDate: null,
        readDate: null,
        rating: 0
      };
      this.book$ = of(book);
    } else {
      this.book$ = this.bookService.getBook(+id);
    }
}

Action buttons from book-detail and book-edit

The next thing we need to do is make the action buttons work in the book-detail component and the book-edit component.

For the book-detail component, we just can add a routerLink to the buttons to make the navigation work.

<mat-card-actions>
    <button color="primary" mat-button [routerLink]="['/books', book.id, 'edit']" >Edit</button>
    <button color="accent" mat-button [routerLink]="['/books']">Back to book list</button>
</mat-card-actions>

For the book-edit component, we can add a routerLink to the Cancel-button. The save and delete buttons need a method in the code.

<mat-card-actions>
    <button color="primary" mat-raised-button (click)="saveBook()" >Save</button>
    <button color="warn" mat-button (click)="deleteBook()">Delete</button>
    <button color="accent" mat-button [routerLink]="['/books']">Cancel</button>
</mat-card-actions>
public saveBook(): void {
  if (this.book.id === 0) {
    this.bookService.createBook(this.book)
      .subscribe({
        next: () => this.router.navigate(['/books']),
        error: err => this.errorMessage = err
      });
  } else {
    this.bookService.updateBook(this.book)
      .subscribe({
        next: () => this.router.navigate(['/books']),
        error: err => this.errorMessage = err
      });
  }
}

public deleteBook(): void {
  this.bookService.deleteBook(this.book.id)
    .subscribe({
      next: () => this.router.navigate(['/books']),
      error: err => this.errorMessage = err
    });
}

Okay, now all our navigation is working. Yeah!

Hmm… There still seems to be one problem. When you go the edit view for a book and you then click “Add new book” in the toolbar, you can see that the URL in the address bar has changed but the view is not updated. So it seems that the ngOnInit method is not re-executed when the route is changed if we stay on the same view. We can solve this with observables. In the code we now have, the data from the route is only read once. When we instead subscribe to the route-data, we can get the data every time it is changed.

In the book-edit component we change the onInit-method to the following:

public ngOnInit(): void {
  this.route.params.subscribe(params => {
    const id = params['id'];
    if (isNaN(+id) && id === "new") {
      this.book = {
        id: 0,
        title: '',
        authors: [],
        description: '',
        publishDate: new Date(),
        publisher: '',
        startReadingDate: null,
        readDate: null,
        rating: 0
      };
      this.authorRequired = true;
      this.pageTitle = "Add book";
    } else {
      this.bookService.getBook(+id)
        .subscribe(book => {
          this.book = book;
          this.book.authors.length !== 0 ? this.authorRequired = false : this.authorRequired = true;
          this.pageTitle = "Edit book";
        });
    }
  })
}

And now everything is finally working.

In this post we explored how we can navigate between the different components in an angular application and how we can pass parameters to a route. In the next post, we are going to explore lazy-loading.

You can find the code on my GitHub.

Lindsey is a .NET Consultant at eMenKa NV where she is focusing on web development. She is a crew-member of Techorama Belgium and the Netherlands and she runs VISUG, The Visual Studio User Group in Belgium.
Leave a Reply

Your email address will not be published. Required fields are marked *