Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Sandeep Paliwal and Kiran Sakhare and Rupesh Kumar and Jayesh Viradiya
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Sandeep Paliwal , Kiran Sakhare , Rupesh Kumar@rukumar ) , and Jayesh Viradiya

Using Router Events To Detect Back And Forward Browser Navigation In Angular 7.0.4

By Ben Nadel on

This morning, while digging into the "retain scroll" feature that was released with Angular 6, I discovered that the Angular team added a "navigationTrigger" property and a "restoredState" property to the NavigationStart Router event. This is an exciting addition to the Router as it finally gives us the ability to differentiate between an imperative navigation (ex, the user clicked a router-link) and a location-change navigation (ex, the user clicked the Back or Forward buttons in the browser chrome). This insight is something that I struggled with in Angular 5 when building my "restore scroll position" polyfill; and, is something that will make custom behaviors like my polyfill much easier to implement.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Every NavigationStart event in the Router is given a unique, monotonically increasing ID. Even navigation events that are considering "going back" or "going forward" within the Browser's history are given a unique ID. What's great about the Router updates in Angular 6 is that the NavigationStart event now also includes the unique ID of the navigation event being restored by a "popstate" event (ie, a non-imperative navigation event).

This restored state ID is provided in a property called, "restoredState". And, we can use the existence of this property to determine if the navigation was triggered in attempt to move to a completely new browser state (the "restoredState" will be null); or, if the navigation was triggered in an attempt to move to an historical browser state.

ASIDE: The NavigationStart event also includes a "navigationTrigger" property to provide a more technical indication of what triggered the navigation; at this time, however, I am not yet sure what additional value there is to knowing why the navigation occurred. I am sure that there are use-cases for its consumption; but, for the time-being, knowing the restored state seems like the most valuable facet of this update.

To explore this Router behavior, I created a simple demo in which you can navigate between three routes, each of which has three anchor links. As you navigate through the app, the details of the NavigationStart event are logged to the console. And, if you go "back" and "forward" through your Browser's history, you will see how the IDs of previous navigation events are presented as the "restoredState" IDs:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { Event as NavigationEvent } from "@angular/router";
  • import { filter } from "rxjs/operators";
  • import { NavigationStart } from "@angular/router";
  • import { Router } from "@angular/router";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template: `
  • <nav class="nav">
  • <a routerLink="./section-a" class="nav__item">Section A</a>
  • <a routerLink="./section-b" class="nav__item">Section B</a>
  • <a routerLink="./section-c" class="nav__item">Section C</a>
  • </nav>
  •  
  • <router-outlet></router-outlet>
  • `
  • })
  • export class AppComponent {
  •  
  • // I initialize the app component.
  • constructor( router: Router ) {
  •  
  • router.events
  • .pipe(
  • // The "events" stream contains all the navigation events. For this demo,
  • // though, we only care about the NavigationStart event as it contains
  • // information about what initiated the navigation sequence.
  • filter(
  • ( event: NavigationEvent ) => {
  •  
  • return( event instanceof NavigationStart );
  •  
  • }
  • )
  • )
  • .subscribe(
  • ( event: NavigationStart ) => {
  •  
  • console.group( "NavigationStart Event" );
  • // Every navigation sequence is given a unique ID. Even "popstate"
  • // navigations are really just "roll forward" navigations that get
  • // a new, unique ID.
  • console.log( "navigation id:", event.id );
  • console.log( "route:", event.url );
  • // The "navigationTrigger" will be one of:
  • // --
  • // - imperative (ie, user clicked a link).
  • // - popstate (ie, browser controlled change such as Back button).
  • // - hashchange
  • // --
  • // NOTE: I am not sure what triggers the "hashchange" type.
  • console.log( "trigger:", event.navigationTrigger );
  •  
  • // This "restoredState" property is defined when the navigation
  • // event is triggered by a "popstate" event (ex, back / forward
  • // buttons). It will contain the ID of the earlier navigation event
  • // to which the browser is returning.
  • // --
  • // CAUTION: This ID may not be part of the current page rendering.
  • // This value is pulled out of the browser; and, may exist across
  • // page refreshes.
  • if ( event.restoredState ) {
  •  
  • console.warn(
  • "restoring navigation id:",
  • event.restoredState.navigationId
  • );
  •  
  • }
  •  
  • console.groupEnd();
  •  
  • }
  • )
  • ;
  •  
  • }
  •  
  • }

With this code, loaded if we execute the following navigation steps:

  • Click "Section A" link.
  • Click "Section B" link.
  • Click "Section C" link.
  • Click browser's "Back Button".

... we will get the following browser output:


 
 
 

 
 Detecting navigation triggers (imperitive vs. back button) using the NavigationStart event in the Angular Router. 
 
 
 

As you can see, as we navigate through the application, each NavigationStart event is listed as an "imperative" navigation action that has no "restoredState" property. However, when we click the Browser's Back button, the trigger is listed as "popstate"; and, there is a "restoredState" property that points back to the ID of the original "/section-b" navigation event.

While it is not really relevant to the demo, here is my App Module for completeness:

  • // Import the core angular services.
  • import { BrowserModule } from "@angular/platform-browser";
  • import { NgModule } from "@angular/core";
  • import { RouterModule } from "@angular/router";
  •  
  • // Import the application components and services.
  • import { AppComponent } from "./app.component";
  • import { SectionAComponent } from "./section-a.component";
  • import { SectionBComponent } from "./section-b.component";
  • import { SectionCComponent } from "./section-c.component";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule({
  • imports: [
  • BrowserModule,
  • RouterModule.forRoot(
  • [
  • {
  • path: "section-a",
  • component: SectionAComponent
  • },
  • {
  • path: "section-b",
  • component: SectionBComponent
  • },
  • {
  • path: "section-c",
  • component: SectionCComponent
  • }
  • ],
  • {
  • // Tell the router to use the hash instead of HTML5 pushstate.
  • useHash: true,
  •  
  • // These aren't necessary for this demo - they are just here to provide
  • // a more natural experience and test harness.
  • scrollPositionRestoration: "enabled",
  • anchorScrolling: "enabled",
  • enableTracing: false
  • }
  • )
  • ],
  • declarations: [
  • AppComponent,
  • SectionAComponent,
  • SectionBComponent,
  • SectionCComponent
  • ],
  • providers: [
  • // CAUTION: We don't need to specify the LocationStrategy because we are setting
  • // the "useHash" property in the Router module above (which will be setting the
  • // strategy for us).
  • // --
  • // {
  • // provide: LocationStrategy,
  • // useClass: HashLocationStrategy
  • // }
  • ],
  • bootstrap: [
  • AppComponent
  • ]
  • })
  • export class AppModule {
  • // ...
  • }

This is really cool! Obviously, the Angular team added this functionality to enable their own "restore scroll" functionality. However, said functionality only works on the primary viewport as far as I understand. As such, I believe that I can use this functionality to greatly improve the stability of my own "restore scroll" polyfill, which works on an arbitrary collection of scrollable containers.

But, that's an exploration for another day.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.