Scroll to top on Angular Router navigation

Posted on

Scroll to top on Angular Router navigation

When I was creating this blog and optimising it for mobile I experienced some default but not so user-friendly behaviour when navigating from one route to the other with Angular.

The problem is that content on mobile can go very deep below the initial viewport height. So when you're scrolling down and you press an internal link to another page, you'll be stuck at that height.

This is somewhat different from standard navigation between pages in a normal webapplication, where the page reloads and you start from the top by default. In a S.P.A. this can easily be solved by scrolling to the top on navigation by using the native window.scroll function:

window.scroll(0,0)

A navigation in routing in Angular 1 and ngRoute or even the ui-router can easily be dectected by listening to the event $routeChangeSuccess or $stateChangeSuccess. So combining these 2 essentials gives us:

// ngRoute:
$rootScope.$on('$routeChangeSuccess', () => {
    $window.scroll(0,0);
});

// ui-router:
$rootScope.$on('$stateChangeSuccess', () => {
    $window.scroll(0,0);
});

I did not find anything similar in the documentation of the router of Angular so I went digging. The fact is that I'm using the Angulartics2 plugin by @luisfarzati to track you guys' behavior :). This is also done on navigation so there must be something similar going on at that plugin. The plugin BTW works great!

Listening to navigation events in Angular

It seems that the Angular v2+ router has an events Observable property which you can subscribe on. Yes, it is as simple as that. Those events can be of any predefined type NavigationStart, NavigationCancel, NavigationEnd or NavigationError. In my case I only needed the NavigationEnd.

In the component that holds your navigation router-outlet you just need to setup the listener, something like this:

import { Component } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';

@Component({
  selector: 'sv-app',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {

  constructor(private router: Router) {

    router.events
      .filter(event => event instanceof NavigationEnd)
      .subscribe((event: NavigationEnd) => {
        window.scroll(0, 0);
      });

  }
}

And that was it! Be aware that the window object might not be available in every context, except the browser. Check this awesome article by @juristr to read more about why you might want to wrap your window object reference!

Please also be careful not to use these events to do business logic, like for example checking if you can navigate to a specific route based on some authentication rules. For those cases you may want to implement guards! More information about guards can be found in this splendid article by @PascalPrecht of Thoughtram.


Leave some feedback please!

Do you have anything to add? Or have some other remarks? Please let me know in the comment section below! Looking forward to the discusion!