routing-and-navigation
In this section, we discuss the various aspects of routing in an Angular app
1. Why Routing is necessary
Modern web applications are SPA (Single-Page Application).
They load just one single page in memory and then all other pages are loaded via JavaScript (but there aren't really the whole page, just portions of them)
The 7-step routing process
This paragraph comes from https://www.jvandemo.com/the-7-step-process-of-angular-router-navigation/
Every time a link is clicked or the browser URL changes, Angular router makes sure your application reacts accordingly.
To accomplish that, Angular router performs the following 7 steps in order:
- Parse: it parses the browser URL the user wants to navigate to
- Redirect: it applies a URL redirect (if one is defined)
- Identify: it identifies which router state corresponds to the URL
- Guard: it runs the guards that are defined in the router state
- Resolve: it resolves the required data for the router state
- Activate: it activates the Angular components to display the page
- Manage: it manages navigation and repeats the process when a new URL is requested
2. Define routes for pages
Implement routing with router-outlet
Angular's router-outlet component is used to match different routes. A component that wishes to implement routing must declare a <router-outlet> in its template.
As the Angular docs mentions:
the router-outlet "acts as a placeholder that Angular dynamically fills based on the current router state"
@Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`
})
export class AppComponent {
}
Defining routes
Then we need to define routes to tell our component how to serve them. Two options for this:
- Define routes in a routing module (e.g:
AppRoutingModule) and import that module inside the one that needs to make use of them (e.g:AppModule) - Define routes in a separate file (e.g:
routes.ts) and then import them in a module making usage of them (e.g:AppModule).
We use the first approach in the following example:
import { Routes } from '@angular/router'
import { EventsListComponent } from './events/events-list.component'
import { EventDetailsComponent } from './events/event-details.component'
const appRoutes: Routes = [
{path: 'events', component: EventsListComponent},
{path: 'events/:id', component: EventDetailsComponent}, // uses an 'id' route parameter
{path: '', redirectTo: '/events', pathMatch: 'full'} // default path => empty path redirecting to
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
ℹ️ N.B: pathMatch can be set either to full (complete match of path) or prefix (path starts with)
⚠️ It is important to define the base path of our app in our index.html file → <base href="/">. Usually this is done automatically when generating the app with the Angular CLI.
⚠️ Angular will try to match routes in the order of appearance in the Routes array we defined. It is important to understand that because in case of conflicting routes, the first matching one is applied (e.g: If we want to navigate to events/new, Angular can't make a difference between path: 'events/new' and path: 'events/:id' → the first one defined will be applied)
Accessing route parameters
To access route parameters, we can use the ActivatedRoute service:
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EventService } from '../shared/event.service';
@Component({
selector: 'app-event-details',
templateUrl: 'event-details.component.html'
})
export class EventDetailsComponent {
event: Event;
constructor(private eventService: EventService, private route: ActivatedRoute) {
}
ngOnInit() {
const eventId: number = this.route.snapshot.params['id'];
this.event = this.eventService.getEvent(eventId);
}
}
3. Navigate to routes in HTML
To navigate in our html code from one component to another, we need to add a routerLink. This can be done on pretty much any HTML element, for example on a div element:
<!-- In events-list.component.html => navigate link to Event Detail -->
<div [routerLink]="['/events', event.id]">Event Detail</div>
We can also conveniently use it in a link tag :
<!-- In event-details.component.html => navigate link to Event List -->
<a [routerLink]="['/events']">Event List</a>
Styling links
💡 We can use the routerLinkActive directive to style an active link with any class eg: routerLinkActive="class1 class2 or [routerLinkActive]="['class1', 'class2']"
4. Navigate from code
To navigate in our ts code from one component to another we need to use Angular's Router service and the navigate method:
import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-event-details',
templateUrl: '<div>
<div>
<h2>Event : {{event.id}}</h2>
<p>Name : {{event.name}}</p>
</div>
<button (click)="goBackToList()">Back to Event List</button>
</div>'
})
export class EventDetailsComponent {
event: Event;
constructor(private eventService: EventService, private router: Router, private route: ActivatedRoute) {
}
ngOnInit() {
const eventId: number = this.route.snapshot.params['id'];
this.event = this.eventService.getEvent(eventId);
}
goBackToList() {
this.router.navigate(['/events']);
}
}
5. Route Guards
5.1. Guarding route activation
To prevent a user from going to a particular page, we need to define a service class or method that implements Angular's CanActivate interface.
The overriden canActivate method needs to return a boolean indicating whether or not the route can be activated. In the following example, we use a custom service.
import { CanActivate } from "@angular/router"
import { Injectable } from "@angular/core"
import { EventService } from '../shared/event.service';
@Injectable()
class EventRouteActivator implements CanActivate {
constructor(private eventService: EventService) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean>|boolean {
const eventExists = !!this.eventService.getEvent(+route.params['id']);
if (!eventExists) {
this.router.navigate(['/404']);
}
return eventExists;
}
}
💡 The + is used to cast the parameter to a number type and the !! to cast to a boolean
⚠️ We must remember to register the EventRouteActivator service as a provider in our module !
Next, in our routes, we need to add the canActivate property on the route we want to protect.
import { EventRouteActivator } from './events/event-details/event-route-activator.service'
const appRoutes: Routes = [
{ path: 'events/new', component: CreateEventComponent },
{ path: 'events', component: EventsListComponent },
{ path: 'events/:id', component: EventDetailsComponent, canActivate: [EventRouteActivator] },
{ path: '404', component: Error404Component },
{ path: '', redirectTo: '/events', pathMatch: 'full'}
];
5.2. Guarding route de-activation
To discourage a user from leaving a particular page, we need to define a service class or method that implements Angular's CanDeactivate interface.
The overriden canDeactivate method needs to return a boolean indicating whether or not the route can be deactivated. In the following example, we use a custom method and register it as a provider in our module.
@NgModule({
...
providers: [
{
provide: 'canDeactivateCreateEvent',
useValue: checkDirtyState
}
]
})
export class EventModule { }
export function checkDirtyState() {
return false;
}
Next, in our routes, we need to add the canDeactivate property on the route we want to protect and indicate the function that implements this responsibility.
const appRoutes: Routes = [
{ path: 'events/new', component: CreateEventComponent, canDeactivate: ['canDeactivateCreateEvent'] },
...
];
6. Pre-loading data with the resolve route handler
The resolve handler allows us to pre-fetch the necessary data for a component or fulfill some checks before the component loads. This is espacially useful when we want to fetch asynchronous data to make sure we display a component once everything is loaded (no partial display of data).
To define a resolver, we need to implement Angular's Resolve interface:
import { Resolve } from "@angular/router"
import { Injectable } from "@angular/core"
import { EventService } from '../shared/event.service';
import { map } from 'rxjs/operators';
@Injectable()
class EventListResolver implements Resolve<any> {
constructor(private eventService: EventService) {}
resolve() {
return this.eventService.getEvents().pipe(map(events => events));
}
}
We register the resolver in our module:
@NgModule({
providers: [
EventListResolver,
...
]
})
export class EventModule { }
Our routes need to call the resolver:
const appRoutes: Routes = [
{ path: 'events', component: EventsListComponent, resolve: { events: EventListResolver } },
...
];
We can now consume our resolved data
import { Route } from '@angular/router';
@Component({
...
})
export class EventListComponent implements OnInit {
constructor(private route: ActivatedRoute) {
}
ngOnInit() {
this.events = this.route.snapshot.data['events'];
}
}
7. Lazy Loading
We can define a feature module for specific functionnality in our application.
💡 A feature module must import the CommonModule but not the BrowserModule (only imported once in our AppModule).
A feature module must also use RouterModule.forChild(routes). Routes of a child are relative to their parent path → e.g: a child route profile to a parent route profile will be computed to /user/profile.
const userRoutes: Routes = [
{ path: 'profile', component: ProfileComponent }
];
@NgModule({
imports: [
CommonModule
RouterModule.forChild(userRoutes)
]
})
export class UserModule {
}
In our parent Module, we define the top level route user and use loadChildren to lazily-load that module.
const appRoutes: Routes = [
{ path: 'user', loadChildren: () => import('./user/user.module').then(m => m.UserModule) }
];
Lazy-loading avoids loading unnecessary files. Files of feature modules are only loaded when the browser reaches those routes.
8. Optimizing exports with barrels
Barrels are simply index.ts files that export all content of a directory. Their purpose is to expose all imports in a directory in a single index file and re-export them.
export * from './create-event.component';
export * from './event-thumbnail.component';
export * from './events-list-resolver.service';
export * from './events-list.component';
export * from './shared/index'; //export inner barrel
Now we can simplify imports easily in consumers of our module:
import { EventsListComponent, EventDetailsComponent, CreateEventComponent, EventRouteActivator, EventListResolver } * from './events/index';