Wednesday, February 12, 2020

Angular Routing Example

Creating A New Component
Right now, there is a simple game list component that shows a list of all the games in the application. Now, we want to build a new component which shows the details of one game at a time. We will want to be able to click on a link, and have the Angular routing work so that we are sent to a detailed view of that game. In other words, we need a new component so that we can route to that component. Here is what the game detail component will look like.

game-detail.css
.col-md-4 {
    display: flex;
    align-items: center;
    justify-content: center;
}
game-detail.component.html
<div class='card'
     *ngIf='game'>
    <div class='card-header'>
        {{pageTitle + ': ' + game.gameName}}
    </div>

    <div class='card-body'>
        <div class='row'>
            <div class='col-md-4'>
                <img class='center-block img-responsive'
                     [style.width.px]='200'
                     [src]='game.imageUrl'
                     [title]='game.gameName'>
            </div>
            <div class='col-md-8'>
                <h5 class="card-title">{{game.gameName}}</h5>
                <p class="card-text">{{game.description}}</p>
                <ul class="list-group">
                    <li class="list-group-item"><b>Part#:</b> {{game.gameCode | lowercase | convertToColon: '-'}}</li>
                    <li class="list-group-item"><b>Released:</b> {{game.releaseDate}}</li>
                    <li class="list-group-item"><b>Cost:</b> {{game.price|currency:'USD':'symbol'}}</li>
                    <li class="list-group-item"><b>Rating:</b>&nbsp;<game-thumb
                            [rating]='game.thumbRating'></game-thumb>
                    </li>
                </ul>
            </div>
        </div>
    </div>

    <div class='card-footer'>
        <button class='btn btn-outline-secondary' (click)='onBack()'>
            <i class='fa fa-chevron-left'></i> Back
        </button>
    </div>
</div>
game-detail.component.ts
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';

import {IGame} from './game';
import {GameService} from './game.service';

@Component({
    templateUrl: './game-detail.component.html',
    styleUrls: ['./game-detail.component.css']
})
export class GameDetailComponent implements OnInit {
    pageTitle = 'Game Detail';
    errorMessage = '';
    game: IGame | undefined;

    constructor(private route: ActivatedRoute,
                private router: Router,
                private gameService: GameService) {
    }

    ngOnInit() {
        const param = this.route.snapshot.paramMap.get('id');
        if (param) {
            const id = +param;
            this.getGame(id);
        }
    }

    getGame(id: number) {
        this.gameService.getGame(id).subscribe(
            game => this.game = game,
            error => this.errorMessage = <any>error);
    }

    onBack(): void {
        this.router.navigate(['/games']);
    }

}
How Angular Routing Works
All views are displayed within one page in an Angular Application. This is what’s known as a Single Page Application, or SPA. The page where all the views are displayed is typically the index.html file. Ours looks like this.
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Classic Games</title>
    <base href="/">

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<game-root></game-root>
</body>
</html>
Configure Routes
Let’s see how to actually configure some routes in Angular. Routing is component based, and therefore the workflow is to first determine which component(s) you would like to have as a routing target. Then, you can define the route(s) as needed. First, we’ll need a HomepageComponent as this will now be displayed by default instead of the games list. This component lives in a home folder and consists of homepage.component.html and homepage.component.ts.
<div class="card">
    <div class="card-header">
        {{pageTitle}}
    </div>
    <div class="card-body">
        <div class="container-fluid">
            <div class="text-center">
                <img src="./assets/images/classic-games.png" class="img-responsive center-block"/>
            </div>
        </div>
    </div>
    <div class="card-footer text-muted text-right">
        <small>Powered by Angular</small>
    </div>
</div>
homepage.component.ts
import { Component } from '@angular/core';

@Component({
  templateUrl: './homepage.component.html'
})
export class HomepageComponent {
  public pageTitle = 'Homepage';
}
In the app.module.ts file, we can now specify this component as the default route so to speak using RouterModule.forRoot().
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {HttpClientModule} from '@angular/common/http';
import {RouterModule} from '@angular/router';

import {AppComponent} from './app.component';
import {HomepageComponent} from './home/homepage.component';
import {GameModule} from './games/game.module';

@NgModule({
    declarations: [
        AppComponent,
        HomepageComponent
    ],
    imports: [
        BrowserModule,
        HttpClientModule,
        RouterModule.forRoot([
            {path: 'home', component: HomepageComponent},
            {path: '', redirectTo: 'home', pathMatch: 'full'},
            {path: '**', redirectTo: 'home', pathMatch: 'full'}
        ]),
        GameModule
    ],
    bootstrap: [AppComponent]
})
export class AppModule {
}
It is in the app.component.ts file where we can set up the routerLink options for the user. In fact, we’ll use the routerLinkActive directive so that when a user navigates to a specific route, that button or link appears as highlighted. Also note the use of the special router-outlet element which is a placeholder that Angular dynamically fills based on the current router state. In other words, whenever a route is activated – the view associated with that route is loaded into the <router-outlet></router-outlet> tag for display.
import {Component} from '@angular/core';

@Component({
    selector: 'game-root',
    template: `
        <div class="container">
            <nav class='navbar navbar-expand-lg'>
                <ul class='nav nav-pills mr-auto'>
                    <li><a class='nav-link' routerLinkActive='active' [routerLink]="['/home']">Home</a></li>
                    <li><a class='nav-link' routerLinkActive='active' [routerLink]="['/games']">Games</a></li>
                </ul>
                <span class="navbar-text">{{pageTitle}}</span>
            </nav>
            <div class='container'>
                <router-outlet></router-outlet>
            </div>
        </div>`,
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    pageTitle = 'Angular Games Viewer';
}
game-list.component.html
In our game list component where we list out all of the games, we can set up route parameter passing as seen with the highlighted code below.
<div class='card'>
    <div class='card-header'>
        {{pageTitle}}
    </div>
    <div class='card-body'>
        <div class="row">
            <div class="col-3">
                <input type="text" [(ngModel)]='listFilter' class="form-control" id="filterInput"
                       placeholder="Type to filter">
            </div>
            <div class="col">
                <div *ngIf='listFilter' class="form-text text-muted">Filtered by: {{listFilter}}</div>
            </div>
        </div>
        <div class='table-responsive'>
            <table class='table'
                   *ngIf='games && games.length'>
                <thead>
                <tr>
                    <th>
                        <button class='btn btn-primary'
                                (click)='toggleImage()'>
                            {{showImage ? 'Hide&nbsp;' : 'Show'}} Image
                        </button>
                    </th>
                    <th>Game</th>
                    <th>Part#</th>
                    <th>Release Date</th>
                    <th>Cost</th>
                    <th>5 Thumb Rating</th>
                </tr>
                </thead>
                <tbody>
                <tr *ngFor='let game of filteredGames'>
                    <td>
                        <img *ngIf='showImage'
                             [src]='game.imageUrl'
                             [title]='game.gameName'
                             [style.width.px]='imageWidth'
                             [style.margin.px]='imageMargin'>
                    </td>
                    <td>
                        <a [routerLink]="['/games', game.gameId]">
                            {{ game.gameName }}
                        </a>
                    </td>
                    <td>{{ game.gameCode | lowercase | convertToColon: '-' }}</td>
                    <td>{{ game.releaseDate }}</td>
                    <td>{{ game.price | currency:'USD':'symbol':'1.2-2' }}</td>
                    <td>
                        <game-thumb [rating]='game.thumbRating'
                                    (ratingClicked)='onRatingClicked($event)'>
                        </game-thumb>
                    </td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>
    <div class="card-footer text-muted text-right">
        <small>Powered by Angular</small>
    </div>
</div>
<div *ngIf='errorMessage'
     class='alert alert-danger'>
    Error: {{ errorMessage }}
</div>
Define Routes With Parameters
Ok the links are in place noted above using the link in the format of <a [routerLink]=”[‘/games’, game.gameId]”>. Note the route is /games and after the comma we pass the id of the game we want to view. In order for this to work, the routes need to be defined somewhere in the application. For these links, we can define the routes in the game.module.ts file using RouterModule.forChild().
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';

import {GameListComponent} from './game-list.component';
import {GameDetailComponent} from './game-detail.component';
import {ConvertToColonPipe} from '../shared/convert-to-colon.pipe';
import {GameDetailGuard} from './game-detail.guard';
import {SharedModule} from '../shared/shared.module';

@NgModule({
    imports: [
        RouterModule.forChild([
            {path: 'games', component: GameListComponent},
            {
                path: 'games/:id',
                canActivate: [GameDetailGuard],
                component: GameDetailComponent
            },
        ]),
        SharedModule
    ],
    declarations: [
        GameListComponent,
        GameDetailComponent,
        ConvertToColonPipe
    ]
})
export class GameModule {
}
The game-detail.component.ts file must also be made aware of the route. To display the correct game, the game detail component reads the parameter from the url. Then, that parameter is used to fetch the correct game from the back-end API we had set up in an earlier tutorial. To get the parameter from the URL, the ActivatedRoute service is used. This is set up as a dependency in the constructor.

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

import {IGame} from './game';
import {GameService} from './game.service';

@Component({
    templateUrl: './game-detail.component.html',
    styleUrls: ['./game-detail.component.css']
})
export class GameDetailComponent implements OnInit {
    pageTitle = 'Game Detail';
    errorMessage = '';
    game: IGame | undefined;

    constructor(private route: ActivatedRoute,
                private router: Router,
                private gameService: GameService) {
    }

    ngOnInit() {
        const param = this.route.snapshot.paramMap.get('id');
        if (param) {
            const id = +param;
            this.getGame(id);
        }
    }

    getGame(id: number) {
        this.gameService.getGame(id).subscribe(
            game => this.game = game,
            error => this.errorMessage = <any>error);
    }

    onBack(): void {
        this.router.navigate(['/games']);
    }

}