Angular 2 - Routing

angular2

// Angular 2 - Routing:

Routes tell the router which views to display when a user clicks a link or 
pastes a URL into the browser address bar.

To define routes, add to app.module.ts:

import { RouterModule }   from '@angular/router';
RouterModule.forRoot([
  {
    path: 'heroes',
    component: HeroesComponent
  }
])

The routes are an array of route definition.  The route definition has the
following parts:

1. path: the router matches this route's path to the URL in the browser address 
   bar (heroes).

2. component: the component that the router should create when navigating to 
   this route.

Next, we need to make the router available.  To do this, we'll add it to 
AppModule imports array:

import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';
import { RouterModule }   from '@angular/router';

import { AppComponent }        from './app.component';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroesComponent }     from './heroes.component';
import { HeroService }         from './hero.service';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot([
      {
        path: 'heroes',
        component: HeroesComponent
      }
    ])
  ],
  declarations: [
    AppComponent,
    HeroDetailComponent,
    HeroesComponent
  ],
  providers: [
    HeroService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}

We use the forRoot method because we're providing a configured router at the 
root of the application. The forRoot method gives us the Router service 
providers and directives needed for routing, and performs the initial navigation 
based on the current browser URL.

// Router Outlet.

If we paste the path, /heroes, into the browser address bar, the router should 
match it to the heroes route and display the HeroesComponent. But where?  We 
have to tell it where by adding a <router-outlet> element to the bottom of the 
template. RouterOutlet is one of the directives provided by the RouterModule. 
The router displays each component immediately below the <router-outlet> as we 
navigate through the application.

We don't really expect users to paste a route URL into the address bar. We add 
an anchor tag to the template which, when clicked, triggers navigation to the 
HeroesComponent.  The revised template looks like this:

// app.component.ts
template: `
   <h1>{{title}}</h1>
   <a routerLink="/heroes">Heroes</a>
   <router-outlet></router-outlet>
 `

Notice the routerLink binding in the anchor tag. We bind the RouterLink 
directive (another of the RouterModule directives) to a string that tells the 
router where to navigate when the user clicks the link.

The AppComponent is now attached to a router and displaying routed views. For 
this reason and to distinguish it from other kinds of components, we call this 
type of component a Router Component.

If we want to redirect the default path (/) to another path:

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

Recall earlier in the chapter that we removed the HeroService from the providers 
array of HeroesComponent and added it to the providers array of AppModule.  That 
move created a singleton HeroService instance, available to all components of 
the application. Angular will inject HeroService and we'll use it here in the 
DashboardComponent.

At this point, we need to update the hero-detail.component.ts (the 
HeroDetailComponent).  We will no longer receive the hero in a parent component 
property binding. The new HeroDetailComponent should take the id parameter from 
the params observable in the ActivatedRoute service and use the HeroService to 
fetch the hero with that id.  First, add the requisite imports:

import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, Params }   from '@angular/router';
import { Location }                 from '@angular/common';

import { HeroService } from './hero.service';

Let's have the ActivatedRoute service, the HeroService and the Location service 
injected into the constructor, saving their values in private fields:

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

We'll also import the switchMap operator to use later with the route parameters 
Observable:

// hero-detail.component.ts
import 'rxjs/add/operator/switchMap';

We tell the class that we want to implement the OnInit interface:

export class HeroDetailComponent implements OnInit { ... }

Inside the ngOnInit lifecycle hook, we use the params observable to extract the 
id parameter value from the ActivatedRoute service and use the HeroService to 
fetch the hero with that id.

ngOnInit(): void {
  this.route.params
    .switchMap((params: Params) => this.heroService.getHero(+params['id']))
    .subscribe(hero => this.hero = hero);
}

Note how the switchMap operator maps the id in the observable route parameters 
to a new Observable, the result of the HeroService.getHero method.

If the user re-navigates to this component while a getHero request is still 
inflight, switchMap cancels that old request before calling HeroService.getHero 
again.

The hero id is a number. Route parameters are always strings. So we convert the 
route parameter value to a number with the JavaScript (+) operator.

The Router manages the observables it provides and localizes the subscriptions. 
The subscriptions are cleaned up when the component is destroyed, protecting 
against memory leaks, so we don't need to unsubscribe from the route params 
Observable.

Open HeroService and add a getHero method that filters the heroes list from 
getHeroes by id:

// hero.service.ts
getHero(id: number): Promise<Hero> {
  return this.getHeroes()
             .then(heroes => heroes.find(hero => hero.id === id));
}

// dashboard.component.html
<a *ngFor="let hero of heroes"  [routerLink]="['/detail', hero.id]"  
    class="col-1-4">

Notice the [routerLink] binding.  Top level navigation in the AppComponent 
template has router links set to fixed paths of the destination routes, 
"/dashboard" and "/heroes".  This time, we're binding to an expression 
containing a link parameters array. The array has two elements, the path of the 
destination route and a route parameter set to the value of the current hero's 
id.

The two array items align with the path and :id token in the parameterized hero 
detail route definition we added to app.module.ts earlier.

// Refactor routes to a Routing Module:

So far, we have almost 20 lines of AppModule devoted to configuring routes.  
Most applications have many more routes and they add guard services to protect 
against unwanted or unauthorized navigations. Routing considerations could 
quickly dominate this module and obscure its primary purpose which is to 
establish key facts about the entire app for the Angular compiler.

We should refactor the routing configuration into its own class.   The current 
RouterModule.forRoot() produces an Angular ModuleWithProviders which suggests 
that a class dedicated to routing should be some kind of module. It should be a 
Routing Module.

By convention the name of a Routing Module contains the word "Routing" and 
aligns with the name of the module that declares the components navigated to.

Create an app-routing.module.ts file as a sibling to app.module.ts. Give it the 
following contents extracted from the AppModule class:

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent }   from './dashboard.component';
import { HeroesComponent }      from './heroes.component';
import { HeroDetailComponent }  from './hero-detail.component';
const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard',  component: DashboardComponent },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes',     component: HeroesComponent }
];
@NgModule({
  imports: [ RouterModule.forRoot(routes) ],
  exports: [ RouterModule ]
})
export class AppRoutingModule {}

Typical of Routing Modules:

1. Pulls the routes into a variable. You might export it in future and it 
   clarifies the Routing Module pattern.

2. Adds RouterModule.forRoot(routes) to imports.

3. Adds RouterModule to exports so that the components in the companion module 
   have access to Router declarables such as RouterLink and RouterOutlet.

4. No declarations! Declarations are the responsibility of the companion module.

5. Adds module providers for guard services if you have them; there are none in 
   this example.

Now delete the routing configuration from AppModule and import the 
AppRoutingModule (both with an ES import statement and by adding it to the 
NgModule.imports list).

The HeroesComponent navigates to the HeroesDetailComponent in response to a 
button click. The button's click event is bound to a gotoDetail method that 
navigates imperatively by telling the router where to go.  This approach requires 
some changes to the component class:

1. Import the router from the Angular router library
2. Inject the router in the constructor (along with the HeroService)
3. Implement gotoDetail by calling the router.navigate method

gotoDetail(): void {
  this.router.navigate(['/detail', this.selectedHero.id]);
}

Note that we're passing a two-element link parameters array — a path and the 
route parameter — to the router.navigate method just as we did in the 
[routerLink] binding back in the DashboardComponent. Here's the fully revised 
HeroesComponent class:

// heroes.component.ts
export class HeroesComponent implements OnInit {
  heroes: Hero[];
  selectedHero: Hero;

  constructor(
    private router: Router,
    private heroService: HeroService) { }

  getHeroes(): void {
    this.heroService.getHeroes().then(heroes => this.heroes = heroes);
  }

  ngOnInit(): void {
    this.getHeroes();
  }

  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }

  gotoDetail(): void {
    this.router.navigate(['/detail', this.selectedHero.id]);
  }
}

The Angular Router provides a routerLinkActive directive we can use to add a 
class to the HTML navigation element whose route matches the active route. All 
we have to do is define the style for it.

// app.component.ts:
template: `
  <h1>{{title}}</h1>
  <nav>
    <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License