Go Back

Manage Scrolls on router-outlets | Angular

Posted by Simar Singh on 2018-05-30


Angular Single Page Applications (SPAs) handle routes with router-outlets, keeping the browser window on the same page (index.html), enriching experience after initial page load with just post-back / ajax calls

However, by preventing the Browser Window from reacting on route change (fetch a new page), now router-outlet (s) must assume the responsibility of what a browser Window usually does on fetching a new pageScrolls are reset on the Window, and the complete DOM is new. Neither the window, nor elements retain any scroll state from the previous page.

How angular Router works is very well explained here. Once a route is activated, by default routing strategy, all navigation changes matching the route [URL]-> {router-outlet, component} are pushed through the same instance of the component activated inside the target /current router-outlet until we navigate out to a parent route, or where a different route-tree.

This means we could store the state of scrolls before a router-outlet activates, and restore the previous state when its destroyed.

Let’s solve this with a directive that targets all router-outlet

 import {Attribute, Directive, ElementRef, Inject, InjectionToken, OnDestroy, OnInit, Optional} from '[@angular/core](http://twitter.com/angular/core "Twitter profile for @angular/core")';
 import {Event, NavigationEnd, Router} from '[@angular/router](http://twitter.com/angular/router "Twitter profile for @angular/router")';
 import {Subscription} from 'rxjs/Subscription';
 import {distinctUntilChanged, filter} from 'rxjs/operators';
 @Directive()({
   selector: 'router-outlet'
 })
 export class RouterOutletDirective implements OnInit, OnDestroy {

  private routerEventsSubscription: Subscription;
  private currentXY: WindowXY;

  constructor(  
    private elementRef: ElementRef,  
    private router: Router  
  ) {  
    this.elementRef = elementRef;  
    this.router = router;  
    this.routerEventsSubscription = null;  
  }

  public ngOnDestroy(): void {
    if (this.routerEventsSubscription) {  
      this.routerEventsSubscription.unsubscribe();  
    }  
    this.windowCoordinates = this.currentXY;  
  }

  public ngOnInit(): void {
    this.currentXY = this.windowCoordinates;  
    this.windowScroll = [0, 0] as WindowXY; // reset window scroll
    this.routerEventsSubscription = this.router.events.pipe(
      filter((event) => event instanceof NavigationEnd),  
      distinctUntilChanged((prev: NavigationEnd, next: NavigationEnd) => next && next.url === prev.url)  
    )  
      .subscribe(  
        (event: Event): void => {  
          const node = this.elementRef.nativeElement.parentNode;  
          node.scrollTop = 0;  
        }  
      );  
  }

  private get windowCoordinates(): WindowXY {
    return [window.scrollX, window.scrollY] as WindowXY;  
  }

  private set windowCoordinates(xy: WindowXY): WindowXY {
    window.scrollTo(xy[0] || 0, xy[1] || 0);  
  }
 }

Notice, every time the route changes, the directive will grab the parent element and set scrollTop to 0 so if the component filling in the outlet has a content overflow with scroll, the scroll reset to top on every route change. (Assuming scroll is from parent-element to router-outlet marked overflow: scroll ).

Also, every time a router-outlet activates, the directive instance will note-down the current coordinates of the browser window, and restore the same coordinates when the router-outlet deactivates. This is useful in case we have multiple outlets, with int he same app, are in multi-app setup

Since the directive subscribes to navigation events, any change in the window.location whether it be through back button, router-navigation etc, the directive resets the scroll in-line with how browser window does when we navigate to a new page.

You could further enhance this directive, to remember last known scroll-positions (window and router-outlet directive‘s parent) for every URL that directive receives from navigation change subscription. To to store url->coordinates we could leverage an angular-service . If you want to remember the last scroll positions to each URL across browser refreshes / page reloads, use history-api and/or local-storage

You may have to deal with secondary outlets differently. Main idea here is the scroll on window and elements on top of router-outlet s need to managed.