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
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
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