Angular Router Events
Introduction
The Angular Router provides a powerful event system that lets you track the lifecycle of navigation from start to finish. These events are incredibly useful for implementing loading indicators, analytics tracking, permissions validation, or any functionality that needs to respond to navigation changes.
Router events are observable sequences that you can subscribe to in order to react to different stages of the navigation process. In this tutorial, we'll explore how to use these events effectively to enhance your Angular applications.
Understanding Router Events
Angular's Router emits a sequence of events during navigation. Each event represents a specific phase in the navigation lifecycle, from the moment navigation starts to the point when it completes or fails.
Key Router Events
Here's a sequential list of the main router events:
- NavigationStart: Triggered when navigation starts
- RouteConfigLoadStart: Triggered when a route configuration starts loading (for lazy-loaded modules)
- RouteConfigLoadEnd: Triggered when a route configuration finishes loading
- RoutesRecognized: Triggered when the router recognizes the route
- GuardsCheckStart: Triggered when the router begins running guards
- ChildActivationStart: Triggered when the router begins activating a child route
- ActivationStart: Triggered when the router begins activating a route
- GuardsCheckEnd: Triggered when the router finishes running guards
- ResolveStart: Triggered when the router begins resolving route data
- ResolveEnd: Triggered when the router finishes resolving route data
- ChildActivationEnd: Triggered when the router finishes activating a child route
- ActivationEnd: Triggered when the router finishes activating a route
- NavigationEnd: Triggered when navigation successfully completes
- NavigationCancel: Triggered when navigation is canceled
- NavigationError: Triggered when navigation fails due to an error
- Scroll: Triggered when the router performs scrolling
Basic Implementation
Let's start with a basic implementation that subscribes to all router events:
import { Component, OnInit } from '@angular/core';
import { Router, Event, NavigationStart, NavigationEnd,
NavigationCancel, NavigationError } from '@angular/router';
@Component({
selector: 'app-root',
template: `
<div *ngIf="loading" class="loading-indicator">Loading...</div>
<router-outlet></router-outlet>
`,
styles: [
`.loading-indicator {
position: fixed;
top: 0;
left: 0;
right: 0;
background: #f8f9fa;
text-align: center;
padding: 10px;
border-bottom: 1px solid #ddd;
z-index: 1000;
}`
]
})
export class AppComponent implements OnInit {
loading = false;
constructor(private router: Router) {}
ngOnInit() {
this.router.events.subscribe((event: Event) => {
switch (true) {
case event instanceof NavigationStart: {
this.loading = true;
break;
}
case event instanceof NavigationEnd:
case event instanceof NavigationCancel:
case event instanceof NavigationError: {
this.loading = false;
break;
}
default: {
break;
}
}
});
}
}
In this example:
- We subscribe to all router events
- When
NavigationStart
occurs, we show a loading indicator - When navigation completes (
NavigationEnd
), gets canceled (NavigationCancel
), or fails (NavigationError
), we hide the loading indicator
Type-Safe Event Filtering
A more efficient approach is to filter for only the events you need:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-navigation-tracker',
template: `<div class="navigation-status">{{status}}</div>`,
})
export class NavigationTrackerComponent implements OnInit, OnDestroy {
status = 'Idle';
private subscription: Subscription = new Subscription();
constructor(private router: Router) {}
ngOnInit() {
// Subscribe to NavigationStart events
this.subscription.add(
this.router.events.pipe(
filter(event => event instanceof NavigationStart)
).subscribe((event: NavigationStart) => {
this.status = `Navigating to ${event.url}`;
console.log('Navigation started to:', event.url);
})
);
// Subscribe to NavigationEnd events
this.subscription.add(
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
this.status = `Navigation complete`;
console.log('Navigation completed at:', event.url);
})
);
}
ngOnDestroy() {
// Clean up subscriptions to prevent memory leaks
this.subscription.unsubscribe();
}
}
This approach:
- Uses RxJS
filter()
to listen only for specific event types - Makes your code cleaner and more efficient
- Properly types each event for better IDE support and type safety
- Properly handles subscription cleanup with
OnDestroy
Practical Examples
Example 1: Progress Bar for Navigation
Let's create a more sophisticated loading indicator that shows the progress of navigation:
import { Component, OnInit } from '@angular/core';
import { Router, Event, NavigationStart, RouteConfigLoadStart,
RouteConfigLoadEnd, RoutesRecognized, GuardsCheckStart,
NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
@Component({
selector: 'app-navigation-progress',
template: `
<div class="progress-container" *ngIf="navigationInProgress">
<div class="progress-bar" [style.width.%]="progressValue"></div>
</div>
`,
styles: [`
.progress-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background-color: #eee;
z-index: 1000;
}
.progress-bar {
height: 100%;
background-color: #3498db;
transition: width 0.2s ease;
}
`]
})
export class NavigationProgressComponent implements OnInit {
navigationInProgress = false;
progressValue = 0;
constructor(private router: Router) {}
ngOnInit() {
this.router.events.subscribe((event: Event) => {
if (event instanceof NavigationStart) {
this.navigationInProgress = true;
this.progressValue = 10;
} else if (event instanceof RouteConfigLoadStart) {
this.progressValue = 20;
} else if (event instanceof RouteConfigLoadEnd) {
this.progressValue = 40;
} else if (event instanceof RoutesRecognized) {
this.progressValue = 60;
} else if (event instanceof GuardsCheckStart) {
this.progressValue = 80;
} else if (event instanceof NavigationEnd) {
this.progressValue = 100;
setTimeout(() => {
this.navigationInProgress = false;
}, 200); // Keep progress bar visible briefly after completion
} else if (event instanceof NavigationCancel || event instanceof NavigationError) {
this.progressValue = 100;
setTimeout(() => {
this.navigationInProgress = false;
}, 200);
}
});
}
}
Example 2: Tracking and Analytics
Router events are perfect for implementing analytics tracking:
import { Injectable } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AnalyticsService {
constructor(private router: Router) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
this.trackPageView(event.urlAfterRedirects);
});
}
private trackPageView(url: string) {
// Replace with your actual analytics tracking code
console.log(`Page view tracked: ${url}`);
// Example Google Analytics tracking
// if (typeof ga === 'function') {
// ga('set', 'page', url);
// ga('send', 'pageview');
// }
}
}
Then in your app.module.ts
:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { AnalyticsService } from './analytics.service';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
RouterModule.forRoot([
// Your routes
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(private analyticsService: AnalyticsService) {
// Service will automatically start tracking when injected
}
}
Example 3: Confirmation Before Navigation
Using router events to implement a "unsaved changes" warning:
import { Component, HostListener } from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
@Component({
selector: 'app-edit-form',
template: `
<form>
<input [(ngModel)]="formData" name="data" />
<button type="submit">Save Changes</button>
</form>
`
})
export class EditFormComponent {
formData = '';
isDirty = false;
constructor(private router: Router) {
this.router.events.pipe(
filter(event => event instanceof NavigationStart)
).subscribe((event: NavigationStart) => {
if (this.isDirty && !window.confirm('You have unsaved changes. Are you sure you want to leave this page?')) {
this.router.navigate([this.router.url]);
}
});
}
@HostListener('input')
onInput() {
this.isDirty = true;
}
onSave() {
// Save data
this.isDirty = false;
}
}
Best Practices
-
Clean up subscriptions: Always unsubscribe from router events when your component is destroyed to prevent memory leaks.
-
Filter events: Use RxJS operators to filter only for the events you need rather than processing all events.
-
Type safety: Make use of TypeScript's type system to work with specific router event types.
-
Central event handling: Consider setting up a central service for handling router events if you need to react to them in multiple places.
-
Debugging: Router events can help you debug navigation issues by logging the sequence of events.
Advanced: Creating a Router Event History Service
import { Injectable } from '@angular/core';
import { Router, Event, NavigationStart, NavigationEnd,
NavigationCancel, NavigationError } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
interface RouterEventRecord {
type: string;
url?: string;
timestamp: number;
id?: number;
}
@Injectable({
providedIn: 'root'
})
export class RouterEventHistoryService {
private _history = new BehaviorSubject<RouterEventRecord[]>([]);
public history$ = this._history.asObservable();
constructor(private router: Router) {
this.router.events.subscribe((event: Event) => {
let record: RouterEventRecord | null = null;
if (event instanceof NavigationStart) {
record = {
type: 'NavigationStart',
url: event.url,
id: event.id,
timestamp: Date.now()
};
} else if (event instanceof NavigationEnd) {
record = {
type: 'NavigationEnd',
url: event.url,
id: event.id,
timestamp: Date.now()
};
} else if (event instanceof NavigationCancel) {
record = {
type: 'NavigationCancel',
url: event.url,
id: event.id,
timestamp: Date.now()
};
} else if (event instanceof NavigationError) {
record = {
type: 'NavigationError',
url: event.url,
id: event.id,
timestamp: Date.now()
};
}
if (record) {
const currentHistory = this._history.getValue();
const updatedHistory = [...currentHistory, record].slice(-10); // Keep last 10 events
this._history.next(updatedHistory);
}
});
}
public clearHistory(): void {
this._history.next([]);
}
}
Summary
Angular Router Events provide a powerful way to track and respond to navigation changes in your application. Key points to remember:
- Router events follow a predictable sequence from
NavigationStart
toNavigationEnd
or error/cancel events - You can use these events for loading indicators, analytics tracking, and confirming navigation
- Proper subscription management is crucial to prevent memory leaks
- Leverage RxJS operators for efficient event handling
- Consider creating centralized services for cross-component navigation concerns
By mastering Router Events, you can create more responsive, user-friendly applications that provide feedback during navigation and handle edge cases gracefully.
Additional Resources
Exercises
- Create a debug service that logs all router events to the console with timestamps.
- Implement a "back to top" feature that scrolls to the top of the page on every navigation.
- Build a breadcrumb component that updates based on the current route.
- Create a service that tracks how long users spend on each page using router events.
- Implement a "recently visited pages" feature that uses router events to build a history of pages the user has visited.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)