How do I detect a route change in Angular?
What a great, and actually very common, question about Angular! One of the fundamental and powerful features of Angular that provides the framework with a considerable advantage over non-framework development, is being an out-of-the-box SPA, or Single Page Application. But with this functionality comes the task of figuring out just how do you use this amazing feature? Let’s dive into it!
🚀 The TL;DR Guide to SPAs
Well hold on there, what even is an SPA?
You may now be thinking to yourself if Angular is a “single page” application, then why/how can I change pages? Clearly, I wouldn’t want a website that is just one page, a mile or two long, that would just be silly. So before we get into discussing Angular route changes, how they work, and further on how to detect them, let’s first start off by actually understanding what an SPA is. If you just want to skip ahead, feel free to click here and go straight to route changing!
For those readers still with me here, a single page application is a term used to refer to a website that effectively “fakes” page changing. Instead of the user clicking on a link and requesting an entirely new HTML/CSS/JS file bundle from the server for each URL location change, Angular and other SPA enabled frameworks come preloaded with most of that information the moment that a user first visits any page on the website. Because the data is minimized, packaged, and served in such a way, page load times can be cut in half or even more!
Routing around using the SPA Way
So using the “SPA way”, when a user clicks a link to another page, the webpage actually just stays where it is and replaces certain content on the screen with new elements through the use of JavaScript. You can see this in action on the Angular Docs Start Page, just load it up and start clicking on items within the left side menu.
A key indicator that a website is more than likely a SPA is the lack of the traditional “white flash”, or “dark flash” for those of you who use your browser’s dark mode 🌚, that you normally get when moving from one webpage to another. You should instead notice that while the content usually near the middle of the screen changes, and in the case of the Angular website it even beautifully fades out while the new content fades in, the left and/or top side menus remain in place and don’t get reloaded at all. This is where SPAs really start to shine.
As soon as the user click’s a link/button to change pages, the page immediately starts to reload with the requested content. No waiting for the request to get to the server, and certainly no waiting for a response to get back. This is because all the static HTML information that the pages need to render is already loaded into your browser as soon as you hit the Angular.io website. Note: this is barring a few specific exceptions based on sub-modules, authentication, lazy-loading, and other specifics but that’s a tale for another time!
Simply put, because the menus should be there for each page and are the same regardless of which specific page you are on, there is no point in trying to reload everything all over again just to look and function exactly the same. Angular instead allows the website to swap in only the new sections required to be changed while leaving everything else in place, which drastically reduces the webpage’s overhead for loading.
Users won’t have to wait for the server to respond with the new information before the page loads. Instead, the page can load immediately with any static information and any potential dynamic data can then be quickly retrieved from the server during the change.
So, now that we at least roughly understand what an SPA is and how it works, let’s get to the main topic of Angular route changes!
Let’s talk about route changing
The Angular router provides developers with a relatively easy way of managing routes and tracking location changes. The built-in router, imported from @angular/router, gives us the events observable that provides a verbose log of all routing events taking place in the Angular application. A Route change detection service is exampled below:
Route change detection using a service
A list of Router events can be found from Angular.io. As of the date of publication, this is an exhaustive list of navigation events.
Pimport { Injectable } from '@angular/core'; import { Router, Event, NavigationStart, RoutesRecognized, RouteConfigLoadStart, RouteConfigLoadEnd, NavigationEnd, NavigationCancel, NavigationError, GuardsCheckStart, ChildActivationStart, ActivationStart, GuardsCheckEnd, ResolveStart, ResolveEnd, ChildActivationEnd, ActivationEnd, Scroll } from '@angular/router'; @Injectable({ providedIn: 'root' }) export class RouteService { constructor(private router: Router) { router.events.subscribe((event: Event) => { if (event instanceof NavigationStart) { console.clear(); // * NavigationStart: Navigation starts. console.log('NavigationStart --- ', event.url); } else if (event instanceof RouteConfigLoadStart) { // * RouteConfigLoadStart: Before the router lazy loads a route configuration. console.log('RouteConfigLoadStart --- ', event.toString()); } else if (event instanceof RouteConfigLoadEnd) { // * RouteConfigLoadEnd: After a route has been lazy loaded. console.log('RouteConfigLoadEnd --- ', event.toString()); } else if (event instanceof RoutesRecognized) { // * RoutesRecognized: When the router parses the URL and the routes are recognized. console.log('RoutesRecognized --- ', event.url); } else if (event instanceof GuardsCheckStart) { // * GuardsCheckStart: When the router begins the guards phase of routing. console.log('GuardsCheckStart --- ', event.url); } else if (event instanceof ChildActivationStart) { // * ChildActivationStart: When the router begins activating a route's children. console.log('ChildActivationStart --- ', event.toString()); } else if (event instanceof ActivationStart) { // * ActivationStart: When the router begins activating a route. console.log('ActivationStart --- ', event.toString()); } else if (event instanceof GuardsCheckEnd) { // * GuardsCheckEnd: When the router finishes the guards phase of routing successfully. console.log('GuardsCheckEnd --- ', event.url); } else if (event instanceof ResolveStart) { // * ResolveStart: When the router begins the resolve phase of routing. console.log('ResolveStart --- ', event.url); } else if (event instanceof ResolveEnd) { // * ResolveEnd: When the router finishes the resolve phase of routing successfully. console.log('ResolveEnd --- ', event.url); } else if (event instanceof ChildActivationEnd) { // * ChildActivationEnd: When the router finishes activating a route's children. console.log('ChildActivationEnd --- ', event.toString()); } else if (event instanceof ActivationEnd) { // * ActivationEnd: When the router finishes activating a route. console.log('ActivationEnd --- ', event.toString()); } else if (event instanceof NavigationEnd) { // * NavigationEnd: When navigation ends successfully. console.log('NavigationEnd --- ', event.url); } else if (event instanceof NavigationCancel) { // * NavigationCancel: When navigation is canceled. console.log('NavigationCancel --- ', event.url); } else if (event instanceof NavigationError) { // * NavigationError: When navigation fails due to an unexpected error. console.log('NavigationError --- ', event.error); } else if (event instanceof Scroll) { // * Scroll: When the user scrolls. console.log('Scroll --- ', event.position); } }); } }
Angular Route Change Practical Example
Why would I want to use this? What’s the benefit?
One example of why you, the developer, might want to track a route change event, is to track the progression of the user throughout the application. One common method of doing this is to create a breadcrumb trail that allows users to quickly see where they are in the application and could also provide the ability to quickly backtrack to a higher point up the site tree without having to endlessly push the back button on their browser.
The following code snippets will represent an example Angular website that is used for students to view their classes, the chapters in each class, and the assignments for each chapter.
Routing Module
... // * By cascading the route url (a/b/c/x/y/z), you create a parent/child relationship within the routes // * The colon is used to identify a route parameter that will be used to dynamically change the content on the page const routes: Routes = [ { path: 'home', component: HomepageComponent, }, { path: 'home/:class', component: ClassComponent, }, { path: 'home/:class/:chapter', component: ChapterComponent, }, { path: 'home/:class/:chapter/:assignment', component: AssignmentComponent, }, { path: '**', redirectTo: '', }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class RoutingModule { } ...
RouteService
... @Injectable({ providedIn: 'root' }) export class RouteService { public breadcrumbString: string = ''; constructor(private router: Router) { router.events.subscribe((event: Event) => { if (event instanceof RoutesRecognized) { // * RoutesRecognized: When the router parses the URL and the routes are recognized. // ! This is the earliest event in the navigation lifecycle that you can begin to update the breadcrumbs this.breadcrumbString = this.getBreadcrumbs(this.findLastFirstChild(event.state.root)); } }); } // * Only the LAST instance of first child has the correct properties to parse breadcrumbs based on route params (different from queryParams) // * Unfortunately, the only way to find the last instance is to recursively deep dive into the firstChild properties. // * (snapshot.firstChild.firstChild.firstChild.etc...) private findLastFirstChild(snapshot: ActivatedRouteSnapshot): ActivatedRouteSnapshot { return snapshot.firstChild ? this.findLastFirstChild(snapshot.firstChild) : snapshot; } private getBreadcrumbs(snapshot: ActivatedRouteSnapshot): string { const breadcrumbs: string[] = []; // * At the root url, url[0] will not exist if (snapshot.url[0]?.path) { breadcrumbs.push(snapshot.url[0]?.path); } // * snapshot.params will be an object with properties that match the route params used for your current route // * Ex: snapshot.params = {class: 'English', chapter: '7', assignment: '4'} for (const [key, value] of Object.entries(snapshot.params)) { breadcrumbs.push(`${key.slice(0, 1).toUpperCase() + key.slice(1)}: ${value.slice(0, 1).toUpperCase() + value.slice(1)}`); // Breadcrumb template = "Key: Value" } return breadcrumbs.join(' > '); // Full breadcrumbs output = "Root > Key: Value > Key: Value > ..." } } ...
app.component.html
<div> {{routeService.breadcrumbString}} <!--Ex: Home > Class: English > Chapter: 7 > Assignment: 4 --> <!-- Some more work would have to be done to enable routing when clicking on a breadcrumb but we will leave that for another time! --> </div>
Wrapping Up
With this service, you can listen to all of the Angular routing events in real time as they happen. The specifically listed event instances are listed in the order that they should occur in during a standard route change. This will enable you to run code on specific events types, capture analytical data, create customized user interaction reports, or even just help debug errors.
With the ever evolving nature of Angular, be sure to check Angular.io from time to ensure that you stay up to date on the latest changes to Angular and definitely make sure to stay up to date on the Briebug Blog for more great information!