Angular 2 – Routing

Setting up and loading routes

  • We set up two routes:
    • http://localhost:4200/page1
    • http://localhost:4200/page2

app.module.ts

import { Routes, RouterModule } from '@angular/Router';
import { Page1Component } from './page1/page1.component';
import { Page2Component } from './page2/page2.component';

const appRoutes: Routes = [
  // { path: '', Page1Component},
  { path: 'page1', component: Page1Component},
  { path: 'page2', component: Page2Component}
];

...
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule.forRoot(appRoutes)
  ],

...

app.component.html

      <router-outlet></router-outlet>

Navigating with Router links

  • Using routerLink directive we avid reloading of the page

app.component.html

<h5>Routing</h5>
 <ul class="nav nav-tabs">
 <li role="presentation" class="active"> <a routerLink="/">Home</a></li>
 <li role="presentation"> <a routerLink="/page1">Page 1</a></li>
 <li role="presentation"> <a [routerLink]="['/page2']">Page 2</a></li>
 </ul>
 ...
 
 <router-outlet></router-outlet>

Understanding navigation paths

  • In routerLinks we can define relative paths to current path:
    <a routerLink="page1">Page 1</a>
  • or absolute paths: 
    <a routerLink="/page1">Page 1</a>
  • or navigate as a folder structure:
    <a routerLink="../page1">Page 1</a>

Styling Active Router Links

      <ul class="nav nav-tabs">
        <li role="presentation" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}"> <a routerLink="/">Home</a></li>
        <li role="presentation" routerLinkActive="active"> <a routerLink="/page1">Page 1</a></li>
        <li role="presentation" routerLinkActive="active"> <a [routerLink]="['/page2']">Page 2</a></li>
      </ul>

Loading routes programmatically

import { Router } from '@angular/Router';
...
goto(target) {
   this.router.navigate([target]);
 }

using relative paths with navigation method:

constructor(private route: ActivatedRoute , private router: Router){}
...
this.router.navigate([target], {relativeTo: this.route});

Passing parameters to routes

app.module.ts

const appRoutes: Routes = [
 // { path: '', Page1Component},
 { path: 'page1', component: Page1Component},
 { path: 'page2', component: Page2Component},
 { path: 'page2/:id/:name', component: Page2Component}
];

page2.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/Router';
@Component({
 selector: 'app-page2',
 templateUrl: './page2.component.html',
 styleUrls: ['./page2.component.css']
})
export class Page2Component implements OnInit {

user: {id: number , name: string};

constructor(private route: ActivatedRoute) { }

ngOnInit() {
 this.user = {
 id: this.route.snapshot.params['id'],
 name: this.route.snapshot.params['name']
 };
 }
}

Subscribing to changes in the route params

this.route.params.subscribe(
 (params: Params) => {
 this.user.id = params.id;
 this.user.name = params.name;
 }
 );

NOTE: The subscription on the params will remain while we are in teh page2 component; Angular2 will automatically remove the subscription on destroy of the component. Just in case you need to unsubscribe for observables we show yo what angular does:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/Router';
import { Subscription } from 'rxjs/Subscription';

@Component({
  selector: 'app-page2',
  templateUrl: './page2.component.html',
  styleUrls: ['./page2.component.css']
})
export class Page2Component implements OnInit, OnDestroy{

  user: {id: number , name: string};
  paramsSubscription: Subscription;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.user = {
      id: this.route.snapshot.params['id'],
      name: this.route.snapshot.params['name']
    };

    this.paramsSubscription = this.route.params.subscribe(
      (params: Params) => {
        this.user.id = params['id'];
        this.user.name = params['name'];
      }
    );
  }

  ngOnDestroy() {
    this.paramsSubscription.unsubscribe();
  }

}

Passing Query Parameters and Fragments

  • From the html, this code
<a
  [routerLink]="['/page2']"
  [queryParams]="{allowEdit: '1', otro: 'jaja'}"
  fragments="myfragment">
    Navigate to Page2 with query params and fragments
</a>

will go to http://localhost:4200/page2?allowEdit=1&otro=jaja

  • From the typescript code:
  gotoWithQueryParamsAndFragments(target) {
    this.router.navigate([target], {relativeTo: this.route, queryParams: {allowEdit:'1'}, fragment: 'myfragment'});
  }

it will go to http://localhost:4200/page2?allowEdit=1#myfragment

  • For retrieving them:
console.log('Init page 2 queryParams and fragments', this.route.snapshot.queryParams, this.route.snapshot.fragment);

and also , as  they are observables we can subscribe to their changes.

NOTE: To convert a string into a number there is a sneaky way: to prepend a ‘+’ operator:

> +'42'
42

Setting up nested routes

  • We set up ‘/page2/nested’ route. This route will be child of /page2 and in the page 2 html we insert “router-outlet” to nest this child views.

app.module.ts

const appRoutes: Routes = [
  // { path: '', Page1Component},
  { path: 'page1', component: Page1Component},
  { path: 'page2', component: Page2Component, children: [
    { path: 'nested', component: NestedComponent}
  ]},
  { path: 'page2/:id/:name', component: Page2Component}

];

page2.component.html

<p>
  page2 works!
</p>
<p>
  User Id: {{user.id}}
</p>
<p>
  User name: {{user.name}}
</p>

<p>
  <a [routerLink]="['/page2', '23', 'INDIRA']">Navigate to Page2/23/INDIRA</a>
</p>

<p>
<a
  [routerLink]="['/page2']"
  [queryParams]="{allowEdit: '1', otro: 'jaja'}"
  fragments="myfragment">
    Navigate to Page2 with query params and fragments
</a>
</p>

<hr>
Nested routes under page2
<router-outlet></router-outlet>

Preserve query parameters on navigation to other route

    this.router.navigate([target], {relativeTo: this.route, queryParamsHandling: 'preserve'});

Redirecting and wildcard routes

We want redirect /page3 to /not-found

and we want to get all unknown routes to /not-found

app.module.ts

const appRoutes: Routes = [
  // { path: '', Page1Component},
  { path: 'page1', component: Page1Component},
  { path: 'page2', component: Page2Component, children: [
    { path: 'nested', component: NestedComponent}
  ]},
  { path: 'page2/:id/:name', component: Page2Component},
  { path: 'not-found', component: PageNotFoundComponent},
  { path: 'page3', redirectTo: '/not-found'},
  { path: '**', redirectTo: '/not-found'}

];

NOTE: the wildcard rule is evaluated at the end because it is in the bottom of the rules. If we put it at the top , all routes will go to not-found.

NOTE 2: Important: Redirection Path Matching
 

In our example, we didn’t encounter any issues when we tried to redirect the user. But that’s not always the case when adding redirections.

By default, Angular matches paths by prefix. That means, that the following route will match both /recipes  and just /

{ path: '', redirectTo: '/somewhere-else' }

Actually, Angular will give you an error here, because that’s a common gotcha: This route will now ALWAYS redirect you! Why?

Since the default matching strategy is "prefix" , Angular checks if the path you entered in the URL does start with the path specified in the route. Of course every path starts with ''  (Important: That’s no whitespace, it’s simply “nothing”).

To fix this behavior, you need to change the matching strategy to"full" :

{ path: '', redirectTo: '/somewhere-else', pathMatch: 'full' }

Now, you only get redirected, if the full path is ''  (so only if you got NO other content in your path in this example).

Outsourcing the router configuration

  • We will get out route configuration from app.module.ts to a proper module:

app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/Router';

import { Page1Component } from './page1/page1.component';
import { Page2Component } from './page2/page2.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { NestedComponent } from './nested/nested.component';

const appRoutes: Routes = [
  // { path: '', Page1Component},
  { path: 'page1', component: Page1Component},
  { path: 'page2', component: Page2Component, children: [
    { path: 'nested', component: NestedComponent}
  ]},
  { path: 'page2/:id/:name', component: Page2Component},
  { path: 'not-found', component: PageNotFoundComponent},
  { path: 'page3', redirectTo: '/not-found'},
  { path: '**', redirectTo: '/not-found'}

];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [RouterModule]

})
export class AppRoutingModule {
}

app.module.ts

...
import { AppRoutingModule } from './app-routing.module';
...

  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule
  ],

...

Protecting Routes with canActivate

  • We want to protect some routes to only logged in users. We will use what Angular calls guards. 
  • They are some services which are called before some routes we set up to.
  • We will simulate an AuthService to loggin in /out

app-routing.module.ts

const appRoutes: Routes = [
  { path: '', component: Page1Component},
  { path: 'page1', component: Page1Component},
  { path: 'page2', canActivate: [AuthGuard], component: Page2Component, children: [
    { path: 'nested', component: NestedComponent}
  ]},
  { path: 'page2/:id/:name', canActivate: [AuthGuard], component: Page2Component},
  { path: 'not-found', component: PageNotFoundComponent},
  { path: 'page3', redirectTo: '/not-found'},
  { path: '**', redirectTo: '/not-found'}

];

auth-guard.service.ts

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

import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isAuthenticated().
    then((authenticated: boolean)=> {
      if(authenticated) {
        return true;
      } else {
        this.router.navigate(['/']);
      }
    });
  }

}

auth.service.ts

export class AuthService {
  loggedIn = false;

  isAuthenticated() {
    const promise = new Promise((resolve,reject)=> {
      setTimeout(()=> {
        resolve(this.loggedIn);
      },800);

    });

    return promise;
  }

  login() {
    this.loggedIn = true;
  }

  logout() {
    this.loggedIn = false;
  }
}

With canActivateChild interface we can protect only the children of a route

Controlling navigation with canDeactivate

  • We will control when leaving /page route with some change to get confirmation from user with a javascript confirm

/page1/can-deactivate-guard.service.ts

import { Observable } from 'rxjs/Observable';
import { CanDeactivate, ActivatedRouteSnapshot , RouterStateSnapshot } from '@angular/router';

export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate,
                currentRoute: ActivatedRouteSnapshot,
                currentState: RouterStateSnapshot,
                nextState?: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return component.canDeactivate();
  }
}

app.routing.module.ts

  ...
{ path: 'page1', component: Page1Component, canDeactivate: [CanDeactivateGuard]},
...

/page1/page1.component.ts

...
export class Page1Component implements OnInit, CanComponentDeactivate {
...
  canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
    console.log('JESSSS confirmLeavingValue', this.confirmLeavingValue);
    if(!this.confirmLeavingValue){
      return true;
    } else {
      return confirm('Do u want to discard the changes?');
    }
  }

app.module.ts

...
  providers: [AccountsService, LoggingService, CounterService , UsersService, AuthService, AuthGuard, CanDeactivateGuard],
...

Passing static data to a Route

  • We want to have a error page generic for any type of error.
  • And we pass the message to show by static data in the route.

app-routing.module.ts

...
{ path: 'not-found', component: ErrorPageComponent, data: {errorMessage: 'Page not found'}},
 // { path: 'not-found', component: PageNotFoundComponent},
...

error-page.component.ts

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

@Component({
  selector: 'app-error-page',
  templateUrl: './error-page.component.html',
  styleUrls: ['./error-page.component.css']
})
export class ErrorPageComponent implements OnInit {

  errorMessage: string;
  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.errorMessage = this.route.snapshot.data['errorMessage'];
    this.route.data.subscribe(
      (data: Data)=> {
        this.errorMessage = data['errorMessage'];
      }
    );
  }

}

error-page.component.html

<h3> {{ errorMessage }}</h3>

Resolving dynamic data with the resolve guard

  • On page2/nested route we will simulate resolve data (a Server data) from a promise before nested component is loaded

example-resolver.service.ts

import {Resolve} from '@angular/Router';
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';

interface Server {
  id: number;
  name: string;
  status: string;
}

@Injectable()
export class ExampleResolver implements Resolve<Server> {

  constructor() {}

  resolve(): Observable<Server> | Promise<Server> | Server {

    return new Promise((resolve, reject) => {
      setTimeout(function(){
        resolve({
          id:1,
          name:'Señor potato',
          status:'single'
        });
      }, 250);
    });

  }

}

app-routing.module.ts

{ path: 'nested', component: NestedComponent, resolve: {server: ExampleResolver}}

nested.component.ts

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

import {ActivatedRoute, Data} from '@angular/Router';

@Component({
  selector: 'app-nested',
  templateUrl: './nested.component.html',
  styleUrls: ['./nested.component.css']
})
export class NestedComponent implements OnInit {

  server: {id:number , name:string, status:string};

  constructor(private route:ActivatedRoute) { }

  ngOnInit() {
    this.route.data.subscribe(
      (data: Data) => {
        this.server = data['server'];
      }
    );
  }

}

nested.component.html

  <h4>Server</h4>
  <ul>
    <li>Id: {{ server.id }}</li>
    <li>Name: {{ server.name }}</li>
    <li>Status: {{ server.status }}</li>
  </ul>

Understanding Location strategies

  • Routes like ‘/page1’ or ‘/page2’ wont work on real web servers , they will get not found error because the server will look for page 1 or page 2 folder.
  • There is a chance to use hash mode (http://localhost:4200/#/page2) in the routes of our app:

app-routing.module.ts

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes, {useHash: true})
  ],
  exports: [RouterModule]

})

dfd

fd

fd

fd
gfgfd

 

fdf

df

df

df

df

d