7. Routing

Before continuing, we will set the 'material' theme to all of our widgets. It will be included to jQWidgets tags as a property with the following syntax:

[theme]="'material'"

We will make our project more rich and with more features.

preview

Create the AppRoutingModule

We will use the familar approach with a command line to create our routing module. Creating it in the top-level module is an Angular best practice.

ng generate module app-routing --flat --module=app

This will generate the app-routing.module.ts file in the src/app folder.

`--flat` puts the file in `src/app` instead of its own folder.
`--module=app` tells the CLI to register it in the `imports` array of the `AppModule`.

The content of the newly created file should looks as that:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
  imports: [
    CommonModule
  ]
})
export class AppRoutingModule { }

We do not need a CommonModule because we do not declare components within it so we can delete the @NgModule.declarations and delete CommonModule references too. Now we will declare Routes in the RouterModule. For this purpose, we should declare both of them from @angular/router library. We will add new metadata to this @NgModule.exports array and within it will add RouterModule. This will make our new component work as router directive, available for use in the AppModule components that will need it. After the changes the AppRoutingModule should look in the following way:

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@NgModule({
  exports: [ RouterModule ]
})
export class AppRoutingModule {}

Create paths (routes)

If we want to navigate to different components easily and to stay on one and the same page - to be a Single Page Application (SPA) - Angular provides Routes. This becomes a ngRoute module that provides you a suitable option to navigate between different 'pages' without reloading the application.

intro

The main properties of one Route are:

  1. path: it is relevant to the URL of the browser address bar and connects it with the [routerLink] that will direct.
  2. component: used to add the component to which this route will navigate.

We will create one array as a constant and also, import the HeroesComponent to which we will navigate throw the route and the browser URL address will look like this: localhost:4200/heroes.

import { HeroesComponent }      from './heroes/heroes.component';
const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

Adding RouterModule.forRoot()

The next step is to change the metadata of the @NgModule and set imports as an array with the routes. This happens easily with the RouterModule.forRoot() set into the array:

imports: [ RouterModule.forRoot(routes) ],

The app-routing.module.ts should have the following changes:

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent }      from './heroes/heroes.component';
const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];
@NgModule({
  imports: [ RouterModule.forRoot(routes) ],
  exports: [ RouterModule ]
})
export class AppRoutingModule {}

Visualize the route component via RouterOutlet

We could determinate the exact place where the components will be visualized through the <router-outlet> element. Now we can replace the <app-heroes> element with it into the AppComponent template:

<h1>{{title}}</h1>
<router-outlet></router-outlet>
<app-messages></app-messages>
The `RouterOutlet` is one of the router directives that became available to the `AppComponent` because `AppModule` imports `AppRoutingModule` which exported `RouterModule`.

If the Angular's server is still running you will see the result.

ng serve

There is only the title and if you add /heroes to the end of the URL will see the difference.

Writing the URL is not the best way to navigate somewhere. For this purpose an anchor element with the attribute - routerLink is used, which will determine the path to the wanted component. The RouterLink directive turns user clicks into router navigation. We will wrap the <a routerLink="/heroes">Heroes</a> element into the <nav> element (src/app/app.component.html):

<h1>{{title}}</h1>
<nav>
        <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

Add the new component

We will add a new component to separate the application in different parts. This will make the routing more sensitive in the application. Again we will use the familiar command to achieve it.

ng generate component dashboard

Open the DashboardComponent template and make the following changes:

<h3>Top Heroes</h3>
<div class="grid grid-pad">
        <a *ngFor="let hero of heroes" class="col-1-4">
        <div class="module hero">
        <h4>{{hero.name}}</h4>
        </div>
        </a>
</div>

Here we will use the familiar ngFor. In this component, we will show part of the Heroes and it will be similar to the Heroes.

getHeroes(): void {
        this.heroService.getHeroes()
        .subscribe(heroes => this.heroes = heroes.slice(1, 5));
}

The DashboardComponent should look like that source code below:
The dashboard.component.html:

<h3>Top Heroes</h3>
<div class="grid grid-pad">
        <a *ngFor="let hero of heroes" class="col-1-4">
        <div class="module hero">
        <h4>{{hero.name}}</h4>
        </div>
        </a>
</div>

The dashboard.component.ts:

import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
        selector: 'app-dashboard',
        templateUrl: './dashboard.component.html',
        styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
  heroes: Hero[];
        constructor(private heroService: HeroService) { }
  ngOnInit() {
        this.getHeroes();
  }
  getHeroes(): void {
        this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes);
  }
}

Adding the new route

We should include the DashboardComponent in the AppRoutingModule and add a new path to the array of routes:

import { HeroesComponent } from './heroes/heroes.component';
import { DashboardComponent } from './dashboard/dashboard.component';
const routes: Routes = [
    { path: 'heroes', component: HeroesComponent },
    { path: 'dashboard', component: DashboardComponent }
];

How to handle the default address of the site

We should handle the case when the URL in the browser's address bar does not specify to which route to be initially navigated. In this case, there is a redirectTo property and we will use it here like this:

{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },

This will redirect the empty path to /dashboard.

We need to navigate back and forth between the DashboardComponent and the HeroesComponent. Add a routerLink to "/dashboard" into the src/app/app.component.html:

<h1>{{title}}</h1>
<nav>
        <a routerLink="/dashboard">Dashboard</a>
        <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

Styling

Let's stylize our view. Firstly we should include the jqxButton component. For this purpose open the tsconfig.json file located in the root directory - the folder before src/ folder. There add "node_modules/jqwidgets-scripts/jqwidgets-ts/angular_jqxbuttons.ts". If you have done this before it is not necessary. We will put the anchor elements inside the jqxButton component. In the app.component.html add the following changes:

<h1>{{title}}</h1>
<nav>
        <jqxButton style="padding: 0;"><a style="margin-top: 0; text-decoration: none;" routerLink="/dashboard">Dashboard</a></jqxButton>
        <jqxButton style="padding: 0;"><a style="margin-top: 0; text-decoration: none;" routerLink="/heroes">Heroes</a></jqxButton>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

The next CSS style adjustment will be in the dashboard.component.css in src/app/app.component.css:

[class*='col-'] {
        float: left;
        padding-right: 20px;
        padding-bottom: 20px;
}
[class*='col-']:last-of-type {
        padding-right: 0;
}
a {
        text-decoration: none;
}
*, *:after, *:before {
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        box-sizing: border-box;
}
h3 {
        text-align: center; margin-bottom: 0;
}
h4 {
        position: relative;
}
.grid {
        margin: 0;
}
.col-1-4 {
        width: 25%;
}
.module {
        padding: 20px;
        text-align: center;
        color: #eee;
        max-height: 120px;
        min-width: 120px;
        background-color: #607D8B;
        border-radius: 2px;
}
.module:hover {
        background-color: #EEE;
        cursor: pointer;
        color: #607d8b;
}
.grid-pad {
        padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
        padding-right: 20px;
}
@media (max-width: 600px) {
        .module {
        font-size: 10px;
        max-height: 75px; }
}
@media (max-width: 1024px) {
        .grid {
        margin: 0;
  }
        .module {
        min-width: 60px;
  }
}

Note: We using the same styles as in the "Angular tutorial - Tour Of Heroes".

Add more options to navigate to the HeroDetailComponent

The HeroDetailComponent is available only from the HeroesComponent. The details appeared when clicking on a row of the jqxGrid. It will be more intuitive if we click on the hero in the Dashboard to see its details, too. Also, another option to navigate to these details should be by a writing of the URL in the browser address bar.

The next step is to delete hero details from HeroesComponent

The <app-hero-detail> element is not necessary to exist in the heroes.component.html of the HeroesComponent because we will use routing for this purpose. Now we will delete the <app-hero-detail> element from there.

After that add a HeroDetailComponent route

We are already familiar with these steps below. Let's open the AppRoutingModule that is contained in src/app/ and do following changes:

We have a new symbol here - colon (:), which is followed by the id of the hero - :id. This is as a placeholder. In this way, we will navigate to the details of a particular hero by this id description.

Now our array of routes should look in this way:

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes', component: HeroesComponent }
];

Connect hero with HeroDetailComponent

Currently, the DashboardComponent hero links do nothing. We will set a parameterized dashboard route. With *ngFor we will iterate all heroes and with hero.id will add a specific parameter to each routerLink. Replace the existing anchor element with this one: <a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}">

Your src/app/dashboard/dashboard.component.html file should be similar as this:

<h3>Top Heroes</h3>
<div class="grid grid-pad">
        <a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}">
        <div class="module hero">
        <h4>{{hero.name}}</h4>
        </div>
        </a>
</div>

Specify concrete hero depending on the id

Before this step, our HeroDetailComponent did not show any interaction after changes. This happens because it displayed the hero that becomes the parent HeroesComponent set to the HeroDetailComponent property.

With the new way to obtain the hero-to-display in the HeroDetailComponent:

Add the following imports to the src/app/hero-detail.component.ts:

import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
import { HeroService }  from '../hero.service';

Inject the ActivatedRoute, HeroService, and Location services into the constructor, saving their values in private fields:

constructor(
        private route: ActivatedRoute,
        private heroService: HeroService,
        private location: Location
) {}

The ActivatedRoute holds information about the route in the instance of the HeroDetailComponent. This component is interested in the route's bag of parameters extracted from the URL. The "id" parameter is the id of the hero to display.

The HeroService gets hero's data from the remote server and this component will use it to get the hero-to-display.

The location is an Angular service for interacting with the browser. You will use it later to navigate back to the view that navigated here.

How to get URL parameters

We will use the ActivatedRoute directive and its options to get the extra parameters in the URL. Add the following changes - add the getHero() method that is invoked in the ngOnInit() lifecycle hook in the src/app/hero-detail/hero-detail.component.ts:

ngOnInit(): void {
        this.getHero();
}
getHero(): void {
        const id = +this.route.snapshot.paramMap.get('id');
        this.heroService.getHero(id)
    .subscribe(hero => this.hero = hero);
}
What happens?

After rebuilding the project with these changes you will get the following error.

ERROR in src/app/hero-detail/hero-detail.component.ts(29,22): error TS2551: Property 'getHero' does not exist on type 'HeroService'. Did you mean 'getHeroes'?
This happens because the getHero()method does not exist.

The route.snapshot is a static image of the route information shortly after the component was created. The paramMap is a dictionary of route parameter values extracted from the URL and with the get() method we specify which parameter we want to get - in this case, "id". The returned value is always a string and for this purpose (+) in front of this value converts it to a number.

Implement HeroService.getHero()

Open the src/app/hero.service.ts and mentioned method:

getHero(id: number): Observable<Hero> {
        this.messageService.add(`HeroService: fetched hero id=${id}`);
        return of(HEROES.find(hero => hero.id === id));
}
Note the backticks (`) that define a JavaScript template literal for embedding the "id".

The getHero() method is an asynchronous operation as the getHeroes(). We are familiar with this already - it returns mock hero as an Observable.

When we click on the Grid's rows nothing happens now. We have a different logic. In this paragraph, you will get to know how to access an URL with navigate() method which is a part of the Router. For this purpose, we will make an update in our HeroesComponent. The first step is to import the Router from '@angular/router':

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

Also, we should add a private property - router into the constructor.

Add following changes in the src/app/heroes/heroes.component.ts file:

import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { Router } from '@angular/router';
@Component({
        selector: 'app-heroes',
        templateUrl: './heroes.component.html',
        styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
  heroes: Hero[];
        constructor(private heroService: HeroService, private router: Router) { }
  ngOnInit() {
        this.getHeroes();
  }
  getHeroes(): void {
        this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes);
  }
  rowdetails(event: any): void {
        let args = event.args;
        let rowData = args.row.bounddata;
        let heroID = rowData.Id;
        this.router.navigate([ './detail/' + heroID.toString() ]);
  }
}

Firstly we will prepare the data. We can get data of currently selected hero from the event object. We need to know the Id of the hero to navigate to its details. Here we use the Router.navigate() method which expects an array with the URL path that we want to navigate. The heroID variable is from type number. We should parse it to string because the URL should be from this type and we should set it to the URL path. After you save these changes you will be able to navigate to the details.

Clear dead code

Now we can delete the unused variables - as selectedHero: Hero; property in the src/app/heroes/heroes.component.ts. It is good practice to clear unused source code. In this way, it will be more readable.

Add 'back' button

It will be better if we can navigate back to hero list or dashboard view. Let add "go back" button on the HeroDetailComponent template:

<jqxButton (click)="goBack()">go back</jqxButton>

We have already imported the relevant references with the familiar steps:

All these steps we did before in the MessagesComponent.

On the click event, we will 'go back' with goBack(). The next step is to implement this option to navigate back to the browser's history stack using Location. Add the goBack() implementation via location.back() into the src/app/hero-detail/hero-detail.component.ts:

goBack(): void {
        this.location.back();
}

Now we can test the achieved result. Let's view the details of the chosen hero from dashboard view or hero list and go back with the button.

RESULT