Changing Pages with Routing
setting up and loading routes
routing allows a SPA's URL to change

it will be the same page but the DOM gets changed

routes are configured in the app module

the const of type Routes contains the desired route

import Routes & RouterModule from angular/router

add RouterModule to imports property of the ngModule decoration

add the method forRoot with the const of type Routes as an arg

the last step registers the routes

    ...
    import { Routes, RouterModule } from '@angular/router';
    ...
    const appRoutes: Routes = [
      { path: 'users', component: UsersComponent },
      { path: '', component: HomeComponent },
      { path: 'servers', component: ServersComponent }
    ];

    @NgModule({
      ...
      imports: [
        ...
        RouterModule.forRoot(appRoutes)
      ],
      ...
    })
    export class AppModule { }
        
using regular html links in a page will cause the page to reload and lose state

use routerLink directive

the router matches the routerLink value with the routes loaded in the app module

    <ul class="nav nav-tabs">
        <li role="presentation" class="active"><a routerLink="/">Home</a></li>
        <li role="presentation"><a routerLink="server">Servers</a></li>
        <li role="presentation"><a ['routerLink']="['users']">Users</a></li>
    </ul>
        

Top

Index

styling active RouterLinks
dynamically add css class active to active tab using routerLinkActive directive

Home tab is always marked active because the Servers and Users paths include the empty path marking Home

to correct this use the Angular property routerLinkActiveOptions to only use the exact path

    <ul class="nav nav-tabs">
        <li role="presentation">
            <a routerLink="/" routerLinkActive='active' [routerLinkActiveOptions]="{exact:true}">Home</a>
        </li>
        <li role="presentation">
            <a routerLink="servers" routerLinkActive='active'>Servers</a>
        </li>
        <li role="presentation">
            <a [routerLink]="['/users']" routerLinkActive='active'>Users</a>
        </li>
    </ul>
        

Top

Index

navigating programmatically
an event in the markup calls a handler in the class

uses absolute pth as denoted by the leading backslash (/)

the method uses the injected router to navigate

    ...
    import {Router} from '@angular/router';
    ...
    export class HomeComponent implements OnInit {

      constructor(private router: Router) { }

      ngOnInit() { }

      onLoadServers(){
        this.router.navigate(['/servers']);
      }
    }
        

Top

Index

using relative paths in programmatic navigation
routerLinks know a lot about the DOM courtesy of the router

this includes the URL so when a relative link is passed the router adds it on to the existing URL

when using the Router's navigate method all paths are treated as absolute because the method does not know the current URL

the navigate method has optional second arg as a javascript object

second arg configures navigation

whether or not using a relative path results in an error or not depends on the route

    this.router.navigate(['servers']);
    this.router.navigate(['/servers']);

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

Top

Index

passing parameters to routes
anything after the slash-colon is a parameter

two routes to the UserComponent with one passing the user id as a param

    const appRoutes: Routes = [
      { path: 'users', component: UsersComponent },
      { path: 'users/:id/:name', component: UserComponent },
      ...
    ];
        
inject the ActivatedRoute into user component

in ngOnInit initialize the user property with the params passed in the URL

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

    @Component({
      selector: 'app-user',
      templateUrl: './user.component.html',
      styleUrls: ['./user.component.css']
    })
    export class UserComponent 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'],
        };
      }
    } 
        

Top

Index

callbacks - fetching route parameters reactively
in the user component markup
    <a [routerLink]="['/users', 10, 'Anna']">Load Anna (10)</a>
        
clicking on the link will change the URL in the browser but the component doesn't update to use the new id and name

when a component calls back to itself with different data in the query string the component does not update itself with the new data

Angular doesn't reload a loaded component and doesn't recognize the changed params

the new params are visible to the component by using route.params.subscribe method which is called every time the route's params change

    ...
    export class UserComponent 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'],
        };
        // there is a callback so update user with new params
        this.route.params.subscribe(
          (params: Params) => {
            this.user.id = params['id'];
            this.user.name = params['name'];
          }
        );
      }
    }
        

Top

Index

important note about route observables
subscriptions are not closely associated with the subscriber

when the component is destroyed the subscription can remain in memory

add a Subsciption property and assign it to the subscription

in the OnDestroy method unsubscribe

    ...
    export class UserComponent implements OnDestroy, OnInit {
      paramsSubscription: Subscription;
      ...

      ngOnInit() {
        ...
        this.paramsSubscription = this.route.params.subscribe(...}
        );
      }
      ngOnDestroy() {
        this.paramsSubscription.unsubscribe();
      }
    } 
        

Top

Index

passing query parameters and fragments
with this route
    ...
    const appRoutes: Routes = [
      ...
      { path: 'servers/:id/edit', component: EditServerComponent }
    ];
    ...
    export class AppModule { }
        
in the component markup add a link with a routerLink property

add queryParams property to pass params as a JSON object containing key value pairs

add fragment property to pass a string in URL

    <div class="list-group">
        <!-- the queryParams and fragment values are properties of the routerLink
        The fragmentcan be written as
            [fragment]="'This is a fragment.'"
            or as
            fragment="This is a fragment."
        -->
        <a [routerLink]="['/servers', 5, 'edit']" 
              [queryParams]="{allowedEdit: '1'}"
              [fragment]="'This is a fragment.'"
              href="#" 
              class="list-group-item" 
              *ngFor="let server of servers">
        {{ server.name }}
        </a>
    </div>
        
to do same thing programatically use an event handler
    ...
    export class HomeComponent implements OnInit {
      constructor(private router: Router,  private route: ActivatedRoute) { }
      ...
      onLoadServer(serverId: number) {
        const queryParams = {allowedEdit: '1'};
        const fragment = 'This is a frgament.';
        this.router.navigate(
            ['servers', serverId, 'edit'], 
            {queryParams: queryParams, fragment: fragment}
        );
      }
    }
        

Top

Index

retrieving query parameters and fragments
path in app.module
    ...
    const appRoutes: Routes = [
      ...
      { path: 'servers/:id/edit', component: EditServerComponent},
    ];
        
inject ActivatedRoute into component

in ngOnInit use this.route.snapshot properties

snapshot is not updated

if component will have a callback new values can be obtained by using subscription methods (see Fetching Route Parameters Reactively)

    import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    ...
    @Component({
      ...
    })
    export class EditServerComponent implements OnInit {
      ...

      constructor(private serversService: ServersService, private route: ActivatedRoute) { }

      ngOnInit() {
        console.log('queryParams : ' + this.route.snapshot.queryParams);
        console.log('fragment : ' + this.route.snapshot.fragment);
        this.route.queryParams.subscribe((queryParams: Params) => {
            this.allowEdit = queryParams['allowEdit'] === '1';
        });
        this.route.fragment.subscribe();

        const id = this.route.snapshot.params['id'];
        this.edit = this.route.snapshot.params[.edit];
        this.server = this.serversService.getServer(id);
        this.serverName = this.server.name;
        this.serverStatus = this.server.status;
      }

      onUpdateServer() {
        this.serversService.updateServer(this.server.id, { name: this.serverName, status: this.serverStatus });
      }
    }  
        

Top

Index

setting up child (nested) routes
servers path has two children
    ...
    const appRoutes: Routes = [
      { path: '', component: HomeComponent },
        path: 'servers', component: ServersComponent, children: [
          { path: ':id', component: ServerComponent },
          { path: ':id/edit', component: EditServerComponent }
        ]
      },
    ];
    ...
    export class AppModule { }
        
the approperiate child component will appear where the router-outlet tags are

    <div class="row">
      <div class="col-xs-12 col-sm-4">
        <div class="list-group">
          <a 
            [routerLink]="['/servers', server.id]" 
            [queryParams]="{allowEdit: '1'}" 
            [fragment]="'loading'"  
            href="#" 
            class="list-group-item"
            *ngFor="let server of servers">
            {{ server.name }}
          </a>
        </div>
      </div>
      <div class="col-xs-12 col-sm-4">
        <router-outlet></router-outlet>
      </div>
    </div>
        

Top

Index

redirecting and wildcard routes
** is a wildcard to be used when no matching paths are found

wildcard path should always be the last path in the array

    ...
    import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

    const appRoutes: Routes = [
      ...
      { path: 'not-found', component: PageNotFoundComponent },
      { path: '**', redirectTo: 'not-found' }
    ];

    @NgModule({
      ...
    })
    export class AppModule { }
        

Top

Index

redirection path matching
by default, Angular matches paths by prefix

that the following route will match both /recipes and just /

        { path: '', redirectTo: '/somewhere-else' }
        
because it always redirects to / Angular will generate an error

to change this behavior set the matching strategy to full

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

Top

Index

outsourcing route configuration
moving routes to their own module reduces the code in app.module

add module to handle routes e.g. app-routing.module.ts

move app.module's Route array to app-routing.module

when app module's appRoute array move it to a different file

forRoot method called because these Routes are used for the root module app.module

    ...
    const appRoutes: Routes = [
        { path: '', component: HomeComponent },
        ...
        { path: 'not-found', component: PageNotFoundComponent },
        { path: '**', redirectTo: 'not-found' }
      ];

    @NgModule({
        imports: [        
            RouterModule.forRoot(appRoutes)
          ],
        exports: [RouterModule]      
    })
    export class AppRoutingModule{} 
        
in app.module add AppRoutingModule to imports property of decoration
    ...
    import { AppRoutingModule } from './app-routing.module';

    @NgModule({
      ...
      imports: [
        ...
        AppRoutingModule
      ],
      ...
    })
    export class AppModule { } 
        

Top

Index

introduction to guards
guards use Router's canActivate to check authorization before allowing path to be accessed

Top

Index

protecting routes with canActivate
mock authentication service
    export class AuthService {
        loggedIn = false;

        isAuthenticated() {
            const promise = new Promise(
                // c'tor takes a method as an arg
                (resolve, reject) => {
                    setTimeout(() => {
                        resolve(this.loggedIn)
                    }, 800);
                }
            );
            return promise;
        }
        ...
    }
        
create a guard for the authService
    import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
    import { Observable } from 'rxjs/Observable';
    import { Injectable } from '@angular/core';

    import { AuthService } from './auth.service';
    @Injectable()
    export class AuthGuard implements CanActivate {

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

        // method can run synchronously or asynchonously
        canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
            // use service 
            return this.authService.isAuthenticated()
                .then(
                    (authenticated: boolean) => {
                        if (authenticated) {
                            return true;
                        } else {
                            // cancel navigation by changing path
                            this.router.navigate(['/']);
                        }
                    }
                );
        }
    }
        
to use set a property of JSON data defining the path
    ...
    import { AuthGuard } from './auth-guard.service';

    const appRoutes: Routes = [
        ...
        {
          path: 'servers', canActivate: [AuthGuard],  component: ServersComponent, children: [
            ...
          ]
        },
        ...
      ];

    @NgModule({
        ...     
    })
    export class AppRoutingModule{}    
        
in app module add new services to providers property

because AuthGuard implements the CanActivate interface the object's canActivate method it is called every time access to the servers path and its children the Router makes the call

Top

Index

protecting child (nested) routes with canActivate
add CanActivateChild interface to AuthGuard
    import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
    ...
    @Injectable()
    export class AuthGuard implements CanActivate, CanActivateChild {
        constructor(private authService: AuthService, private router: Router) { }

        // method can run synchronously or asynchonously
        canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
            ...
        }
        // method can run synchronously or asynchonously
        canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
            return this.canActivate(route, state);
        }
    } 
        
to use
    ...
    const appRoutes: Routes = [
        ...
        {
          path: 'servers', 
          // canActivate: [AuthGuard],  
          canActivateChild: [AuthGuard],  
          component: ServersComponent, 
            children: [
              { path: ':id', component: ServerComponent },
              { path: ':id/edit', component: EditServerComponent }
          ]
        },
        ...
      ];

    ...
    export class AppRoutingModule {}
        

Top

Index

controlling navigation with canDeactivate
implement CanComponentDeactivate as an interface and CanDeactivateGuard as a class implementing the interface
    import { Observable } from 'rxjs/Observable';
    import {ActivatedRouteSnapshot,CanDeactivate,RouterStateSnapshot } from '@angular/router';

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

    export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
        canDeactivate(
            component: CanComponentDeactivate, 
            curentRoute: ActivatedRouteSnapshot,
            currentState: RouterStateSnapshot,
            nextState?: RouterStateSnapshot) : Observable<boolean> | Promise<boolean> | boolean{
                return component.canDeactivate();
            }
    }
        
to use with a route
    ...
    import { CanDeactivateGuard } from './servers/edit-server/can-deactivate-guard.service';

    const appRoutes: Routes = [
        ...
        {
          path: 'servers', 
          // canActivate: [AuthGuard],  
          canActivateChild: [AuthGuard],  
          component: ServersComponent, 
            children: [
              { path: ':id', component: ServerComponent },
              { path: ':id/edit', component: EditServerComponent, canDeactivate: [CanDeactivateGuard] }
          ]
        },
        ...
      ];

    @NgModule({
        imports: [        
            RouterModule.forRoot(appRoutes)
          ],
        exports: [RouterModule]      
    })
    export class AppRoutingModule {} 
        
in the app module import CanDeactivateGuard and add it to the providers array

the type implementing the canActivate interface as shown in path

    ...
    import { CanComponentDeactivate } from './can-deactivate-guard.service';

    @Component({
      selector: 'app-edit-server',
      ...
    })
    export class EditServerComponent implements OnInit, CanComponentDeactivate {
      server: { id: number, name: string, status: string };
      serverName = '';
      serverStatus = '';
      allowEdit = false;
      changesSaved = false;
      ...
      canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
        if (!this.allowEdit) {
          return true;
        }
        if ((this.serverName !== this.server.name || this.serverStatus !== this.server.status) && !this.changesSaved) {
          return confirm('Do you want to discard the changes?');
        }
        return true;
      }
    }  
        
now when the user attempts to navigate away from the edit-server component, the router will call a CanDeactivateGuard instance's canDeactivate method

the guard in turn calls the component argument's canDeactivate method

navigation depends upon what the canDeactivate method returns

Top

Index

passing static data to a route
the error component has a property named errorMessage
    {{ errorMessage }}
        
note static message in the data JSON object
    import {NgModule} from '@angular/core';
    ...
    import { ErrorPageComponent} from './error-page/error-page.component';

    const appRoutes: Routes = [
        ...
        { path: 'not-found', component: ErrorPageComponent, data: {message: 'Page not found.'} },
        ...
      ];
    ...
    export class AppRoutingModule{}
        
the errorMessage property is set in ngOnInit
    import { Component, OnInit } from '@angular/core';
    import {ActivatedRoute, Data} from '@angular/router';

    @Component({
        ...
    })
    export class ErrorPageComponent implements OnInit {
        errorMessage: string;

        constructor(private route: ActivatedRoute) { }

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

Top

Index

resolving dynamic data with the resolve guard
the router can use a resolver to obtain data before navigation occurs
    import { ActivatedRouteSnapshot, RouterStateSnapshot, Resolve } from '@angular/router';
    import { Observable } from 'rxjs/Observable';
    import { Injectable } from '@angular/core';

    import { ServersService } from '../servers.service';

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

    @Injectable()
    export class ServerResolver implements Resolve<server> {

        constructor(private serversService: ServersService) { }

        resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Server> | Promise<Server> | Server {
            return this.serversService.getServer(route.params['id']);
        }
    }  
        
in app-routing module edit the id path adding the resolve property

the ServerResolver will be called before the route is navigated

    ...
    import { ServerResolver } from './servers/server/server-resolver.service';

    const appRoutes: Routes = [
      ...
      {
        path: 'servers',
        // canActivate: [AuthGuard],  
        canActivateChild: [AuthGuard],
        component: ServersComponent,
        children: [
          { path: ':id', component: ServerComponent, resolve: {server: ServerResolver} },
          { path: ':id/edit', component: EditServerComponent, canDeactivate: [CanDeactivateGuard] }
        ]
      },
      ...
    ];
    ...
    export class AppRoutingModule {}
        
need to ServerResolver as a provider in app module

inject the ServerResolver into the ServerComponent

in ngOnInit subscribe to the ActivatedRoute.data property and assign that value to the component's server property

    ...
    import { ServersService } from '../servers.service';

    @Component({
      ...
    })
    export class ServerComponent implements OnInit {
      server: { id: number, name: string, status: string };

      constructor(
        private serversService: ServersService,
        private route: ActivatedRoute,
        private router: Router) { }

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

Top

Index

understanding location strategies
host server perses URLs before Angular does

server musyt be configured to let Angular handle its own 404 errors

server must pass the URL to index.html and let Angular handle it

older technique is to use hash signs in the routes

to enable hash signs in app-routing.module add json object as arg

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

    ...

    const appRoutes: Routes = [
      ...
    ];

    @NgModule({
      imports: [
        RouterModule.forRoot(appRoutes, {useHash: true})
      ],
      exports: [RouterModule]
    })
    export class AppRoutingModule {}   
        
the server will ignore everything after the hashtag and pass the data to Angular

Top

Index

navigation
use routerLinks

use routerLinkActive directive to define the class to be conditionally applied to an element

    <li routerLinkActive="active"><a routerLink="/recipes">Recipes</a></li>
        

Top

Index

use console to create component
to create a component
    ng g c <path>/<name> --spec false
        

Top

Index

n4jvp.com